phase 1-6 implemented

This commit is contained in:
dimitar committed 2026-03-10 04:14:03 +01:00
1 parent 22c274bb83
commit 2c4927478a
73 files changed
+9702 -2354

No files matched your search

+121 -85
View File
@@ -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,
},
})
});