import { Ionicons } from '@expo/vector-icons'; import { useFocusEffect } from '@react-navigation/native'; import { useRouter } from 'expo-router'; import { useCallback, useState } from 'react'; import { ActivityIndicator, Pressable, StyleSheet, View } from 'react-native'; import { AppHeader, AppText, AttendanceRing, Button, Card, EmptyState, InlineBanner, ProgressBar, Screen, StatusPill, SubjectBadge, colors, spacing, } from '@/components/ui'; import { attendanceMessage, attendanceTone, subjectToneFor } from '@/lib/design'; import { collegeApi } from '@/lib/api'; type Subject = Awaited>['subjects'][number]; export default function AttendanceScreen() { const router = useRouter(); const [subjects, setSubjects] = useState([]); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(''); const load = useCallback(() => { setLoading(true); setLoadError(''); collegeApi.attendanceSummary() .then(({ subjects: result }) => setSubjects(result)) .catch((error: Error) => { setSubjects([]); setLoadError(error.message); }) .finally(() => setLoading(false)); }, []); useFocusEffect(useCallback(() => { load(); }, [load])); const totalHeld = subjects.reduce((total, subject) => total + subject.total, 0); const totalAttended = subjects.reduce((total, subject) => total + subject.attended, 0); const totalMissed = subjects.reduce((total, subject) => total + subject.absent, 0); const overall = totalHeld ? Math.round(totalAttended / totalHeld * 100) : 0; const overallTone = attendanceTone(overall, totalHeld); const insight = attendanceMessage(overall, totalHeld); const sortedSubjects = [...subjects].sort((left, right) => { if (!left.total && right.total) return 1; if (left.total && !right.total) return -1; return left.percentage - right.percentage || left.name.localeCompare(right.name); }); return {loadError ? } style={styles.firstBlock} /> : null} {loading ? Calculating attendance… : null} {!loading && !loadError ? <> {insight.title} {insight.message} {totalHeld ? : null} By subject Subjects needing attention appear first {subjects.length === 0 ? : {sortedSubjects.map((subject, index) => { const tone = subjectToneFor(subject.id, subject.color); const palette = colors.subject[tone]; const state = attendanceTone(subject.percentage, subject.total); const atRisk = subject.total > 0 && state !== 'success'; return router.push(`/subjects/${subject.id}` as never)} style={({ pressed }) => [styles.subject, pressed && styles.subjectPressed]}> {subject.name} {subject.code} · {subject.attended}/{subject.total} attended {subject.total ? `${subject.percentage}%` : '—'} {atRisk ? : null} {index < sortedSubjects.length - 1 ? : null} ; })} } : null} ; } function Stat({ value, label }: { value: number; label: string }) { return {value} {label} ; } const styles = StyleSheet.create({ content: { paddingTop: spacing[1], paddingBottom: spacing[9] }, firstBlock: { marginTop: spacing[6] }, loading: { minHeight: 144, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: spacing[3] }, hero: { marginTop: spacing[6], flexDirection: 'row', alignItems: 'center', gap: spacing[6], borderRadius: 20 }, heroCopy: { flex: 1 }, heroMessage: { marginTop: spacing[2] }, heroStatus: { marginTop: spacing[4] }, stats: { minHeight: 92, marginTop: spacing[3], flexDirection: 'row', alignItems: 'center' }, stat: { flex: 1, alignItems: 'center', paddingVertical: spacing[5] }, statDivider: { width: 1, height: 36, backgroundColor: colors.neutral.divider }, statLabel: { marginTop: spacing[1], textAlign: 'center' }, tabular: { fontVariant: ['tabular-nums'] }, sectionHeader: { marginTop: spacing[8], marginBottom: spacing[4] }, sectionSub: { marginTop: spacing[1] }, subjectList: { overflow: 'hidden' }, subject: { minHeight: 100, flexDirection: 'row', alignItems: 'center', gap: spacing[3], padding: spacing[4] }, subjectPressed: { backgroundColor: colors.brand.skySoft }, subjectBody: { flex: 1 }, subjectTop: { flexDirection: 'row', alignItems: 'flex-start', gap: spacing[3] }, subjectTitleWrap: { flex: 1 }, subjectMeta: { marginTop: spacing[1] }, percentage: { fontVariant: ['tabular-nums'] }, progress: { marginTop: spacing[3] }, subjectStatus: { marginTop: spacing[3] }, subjectDivider: { position: 'absolute', left: 72, right: spacing[4], bottom: 0, height: 1, backgroundColor: colors.neutral.divider }, });