phase 1-6 implemented
This commit is contained in:
1 parent
22c274bb83
commit
2c4927478a
73 files changed
+9702
-2354
No files matched your search
@@ -1,18 +1,28 @@
|
||||
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 {
|
||||
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 { getErrorMessage } from "../../utils/error-helpers";
|
||||
import log from "../../utils/logger";
|
||||
|
||||
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
|
||||
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) {
|
||||
@@ -28,75 +38,75 @@ export default function AttendanceScreen() {
|
||||
duration: 1000,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
])
|
||||
)
|
||||
pulse.start()
|
||||
return () => pulse.stop()
|
||||
]),
|
||||
);
|
||||
pulse.start();
|
||||
return () => pulse.stop();
|
||||
}
|
||||
}, [activeCheckIn])
|
||||
}, [activeCheckIn]);
|
||||
|
||||
const fetchAttendance = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const token = await getToken()
|
||||
if (!token) return
|
||||
setLoading(true);
|
||||
const token = await getToken();
|
||||
if (!token) return;
|
||||
|
||||
console.log('Fetching attendance history')
|
||||
const data = await attendanceApi.getHistory(token)
|
||||
setHistory(data)
|
||||
log.debug("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])
|
||||
setActiveCheckIn(data[0]);
|
||||
} else {
|
||||
setActiveCheckIn(null)
|
||||
setActiveCheckIn(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching attendance:', error)
|
||||
Alert.alert('Error', 'Failed to load attendance data')
|
||||
log.error("Failed to fetch attendance", error);
|
||||
Alert.alert("Error", "Failed to load attendance data");
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAttendance()
|
||||
}, [])
|
||||
fetchAttendance();
|
||||
}, []);
|
||||
|
||||
const handleCheckIn = async () => {
|
||||
try {
|
||||
const token = await getToken()
|
||||
if (!token) return
|
||||
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')
|
||||
await attendanceApi.checkIn("gym", token);
|
||||
fetchAttendance();
|
||||
Alert.alert("Success", "Checked in successfully!");
|
||||
} catch (error: unknown) {
|
||||
log.error("Failed to check in", error);
|
||||
Alert.alert("Error", getErrorMessage(error, "Failed to check in"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckOut = async () => {
|
||||
try {
|
||||
const token = await getToken()
|
||||
if (!token) return
|
||||
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')
|
||||
await attendanceApi.checkOut(token);
|
||||
fetchAttendance();
|
||||
Alert.alert("Success", "Checked out successfully!");
|
||||
} catch (error: unknown) {
|
||||
log.error("Failed to check out", error);
|
||||
Alert.alert("Error", getErrorMessage(error, "Failed to check out"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (loading && !history.length) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<ActivityIndicator size="large" color={theme.colors.primary} />
|
||||
</View>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -114,7 +124,7 @@ export default function AttendanceScreen() {
|
||||
<View style={styles.actionContainer}>
|
||||
{activeCheckIn ? (
|
||||
<LinearGradient
|
||||
colors={['rgba(16, 185, 129, 0.15)', 'rgba(5, 150, 105, 0.1)']}
|
||||
colors={["rgba(16, 185, 129, 0.15)", "rgba(5, 150, 105, 0.1)"]}
|
||||
style={[styles.activeCard, theme.shadows.medium]}
|
||||
>
|
||||
<View style={styles.activeCardContent}>
|
||||
@@ -129,7 +139,11 @@ export default function AttendanceScreen() {
|
||||
<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' })}
|
||||
Since{" "}
|
||||
{new Date(activeCheckIn.checkInTime).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -140,7 +154,12 @@ export default function AttendanceScreen() {
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={[styles.checkOutButton, theme.shadows.medium]}
|
||||
>
|
||||
<Ionicons name="log-out-outline" size={20} color="#fff" style={{ marginRight: 8 }} />
|
||||
<Ionicons
|
||||
name="log-out-outline"
|
||||
size={20}
|
||||
color="#fff"
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Text style={styles.buttonText}>Check Out</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
@@ -154,7 +173,12 @@ export default function AttendanceScreen() {
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={[styles.checkInButton, theme.shadows.glow]}
|
||||
>
|
||||
<Ionicons name="log-in-outline" size={24} color="#fff" style={{ marginRight: 8 }} />
|
||||
<Ionicons
|
||||
name="log-in-outline"
|
||||
size={24}
|
||||
color="#fff"
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Text style={styles.checkInButtonText}>Check In</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
@@ -166,13 +190,17 @@ export default function AttendanceScreen() {
|
||||
{history.map((item) => (
|
||||
<LinearGradient
|
||||
key={item.id}
|
||||
colors={['rgba(255, 255, 255, 1)', 'rgba(249, 250, 251, 1)'] as const}
|
||||
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}
|
||||
colors={
|
||||
item.checkOutTime
|
||||
? theme.gradients.success
|
||||
: theme.gradients.primary
|
||||
}
|
||||
style={styles.historyIcon}
|
||||
>
|
||||
<Ionicons
|
||||
@@ -191,18 +219,26 @@ export default function AttendanceScreen() {
|
||||
</View>
|
||||
<View style={styles.timeContainer}>
|
||||
<Text style={styles.historyTime}>
|
||||
In: {new Date(item.checkInTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
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' })}
|
||||
Out:{" "}
|
||||
{new Date(item.checkOutTime).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</LinearGradient>
|
||||
))}
|
||||
</ScrollView>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -215,8 +251,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
centered: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
header: {
|
||||
paddingTop: 60,
|
||||
@@ -227,14 +263,14 @@ const styles = StyleSheet.create({
|
||||
borderBottomRightRadius: theme.borderRadius.xl,
|
||||
},
|
||||
title: {
|
||||
fontSize: theme.typography.fontSize['3xl'],
|
||||
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)',
|
||||
color: "rgba(255, 255, 255, 0.9)",
|
||||
},
|
||||
actionContainer: {
|
||||
marginBottom: 32,
|
||||
@@ -244,9 +280,9 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 20,
|
||||
paddingHorizontal: 24,
|
||||
borderRadius: theme.borderRadius.xl,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
checkInButtonText: {
|
||||
color: theme.colors.white,
|
||||
@@ -257,9 +293,9 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginTop: 16,
|
||||
},
|
||||
buttonText: {
|
||||
@@ -271,11 +307,11 @@ const styles = StyleSheet.create({
|
||||
padding: 20,
|
||||
borderRadius: theme.borderRadius.xl,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(16, 185, 129, 0.2)',
|
||||
borderColor: "rgba(16, 185, 129, 0.2)",
|
||||
},
|
||||
activeCardContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: 16,
|
||||
},
|
||||
activeIconContainer: {
|
||||
@@ -285,8 +321,8 @@ const styles = StyleSheet.create({
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
activeTextContainer: {
|
||||
flex: 1,
|
||||
@@ -313,15 +349,15 @@ const styles = StyleSheet.create({
|
||||
borderRadius: theme.borderRadius.xl,
|
||||
marginBottom: 12,
|
||||
marginHorizontal: 20,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(59, 130, 246, 0.1)',
|
||||
borderColor: "rgba(59, 130, 246, 0.1)",
|
||||
},
|
||||
historyLeft: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
},
|
||||
historyIconContainer: {
|
||||
@@ -331,8 +367,8 @@ const styles = StyleSheet.create({
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
dateText: {
|
||||
fontSize: theme.typography.fontSize.base,
|
||||
@@ -345,10 +381,10 @@ const styles = StyleSheet.create({
|
||||
marginTop: 2,
|
||||
},
|
||||
timeContainer: {
|
||||
alignItems: 'flex-end',
|
||||
alignItems: "flex-end",
|
||||
},
|
||||
historyTime: {
|
||||
fontSize: theme.typography.fontSize.sm,
|
||||
color: theme.colors.gray700,
|
||||
},
|
||||
})
|
||||
});
|
||||
Reference in new issue
Block a user