fitaiProto/apps/mobile/src/app/(tabs)/attendance.tsx
2026-03-11 02:38:43 +01:00

395 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 { AttendanceCalendar } from "../../components/AttendanceCalendar";
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;
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;
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]);
} else {
setActiveCheckIn(null);
}
} catch (error) {
log.error("Failed to fetch 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: 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;
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 (
<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>
<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>
{/* Attendance Calendar */}
{history.length > 0 && <AttendanceCalendar attendanceRecords={history} />}
<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,
},
});