import { Ionicons } from '@expo/vector-icons'; import * as Haptics from 'expo-haptics'; import { useFocusEffect, useRouter } from 'expo-router'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Alert, Platform, Pressable, ScrollView, StyleSheet, View } from 'react-native'; import { AddClassModal } from '@/components/add-class-modal'; import { AppHeader, AppText, BottomSheet, Button, Card, CurrentTimeIndicator, EmptyState, IconButton, InlineBanner, RecessCard, ScheduleEventCard, Screen, WeekStrip, colors, radius, size, spacing, type WeekDay, } from '@/components/ui'; import { addDays, currentMinutes, dateKey, formatDayHeading, mondayOfWeek, timeToMinutes } from '@/lib/date'; import { attendanceMessage, attendanceTone, subjectToneFor } from '@/lib/design'; import { collegeApi, type AttendanceStatus, type Profile, type Session } from '@/lib/api'; type DayMarker = WeekDay['marker']; type AttendanceSummary = { total: number; attended: number; percentage: number }; const emptySummary: AttendanceSummary = { total: 0, attended: 0, percentage: 0 }; const startOfMonth = (date: Date) => new Date(date.getFullYear(), date.getMonth(), 1); const daysInMonth = (date: Date) => new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(); export default function TodayScreen() { const router = useRouter(); const [activeDate, setActiveDate] = useState(() => new Date()); const [calendarMonth, setCalendarMonth] = useState(() => startOfMonth(new Date())); const [classes, setClasses] = useState([]); const [profile, setProfile] = useState(null); const [summary, setSummary] = useState(emptySummary); const [weekendSchedule, setWeekendSchedule] = useState(false); const [weekMarkers, setWeekMarkers] = useState>({}); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(''); const [refresh, setRefresh] = useState(0); const [calendarOpen, setCalendarOpen] = useState(false); const [addClassOpen, setAddClassOpen] = useState(false); const [selectedClass, setSelectedClass] = useState(null); const today = new Date(); const todayKey = dateKey(today); const selectedDateKey = dateKey(activeDate); const weekStart = useMemo(() => mondayOfWeek(activeDate), [activeDate]); const weekDates = useMemo(() => Array.from({ length: 7 }, (_, index) => addDays(weekStart, index)), [weekStart]); useFocusEffect(useCallback(() => { let active = true; collegeApi.profile().then((result) => { if (!active) return; setProfile(result); setWeekendSchedule(Boolean(result.weekendSchedule)); if (!result.weekendSchedule) setActiveDate((current) => current.getDay() === 0 || current.getDay() === 6 ? mondayOfWeek(current) : current); }).catch(() => undefined); return () => { active = false; }; }, [])); const loadSchedule = useCallback(() => { setLoading(true); setLoadError(''); collegeApi.schedule(selectedDateKey) .then(({ sessions }) => setClasses(sessions)) .catch((error: Error) => { setClasses([]); setLoadError(error.message); }) .finally(() => setLoading(false)); }, [selectedDateKey]); useEffect(() => { loadSchedule(); }, [loadSchedule, refresh]); useEffect(() => { collegeApi.attendanceSummary().then(({ subjects }) => { const total = subjects.reduce((value, subject) => value + subject.total, 0); const attended = subjects.reduce((value, subject) => value + subject.attended, 0); setSummary({ total, attended, percentage: total ? Math.round(attended / total * 100) : 0 }); }).catch(() => setSummary(emptySummary)); }, [refresh]); useEffect(() => { Promise.all(weekDates.map((date) => collegeApi.schedule(dateKey(date)).then(({ sessions }) => ({ date, sessions })).catch(() => ({ date, sessions: [] as Session[] })))) .then((results) => { const markers: Record = {}; results.forEach(({ date, sessions }) => { const key = dateKey(date); if (key > todayKey) return; const held = sessions.filter((session) => session.status !== 'cancelled'); if (!held.length) return; if (held.every((session) => session.status === 'attended')) markers[key] = 'success'; else if (held.some((session) => session.status === 'absent')) markers[key] = 'danger'; else if (held.some((session) => session.status === 'pending')) markers[key] = 'warning'; else markers[key] = 'neutral'; }); setWeekMarkers(markers); }); }, [refresh, todayKey, weekDates]); const visibleDays: WeekDay[] = weekDates .filter((date) => weekendSchedule || (date.getDay() !== 0 && date.getDay() !== 6)) .map((date) => ({ date, marker: weekMarkers[dateKey(date)] ?? 'none' })); const updateStatus = (session: Session, status: AttendanceStatus) => { const previous = classes; setClasses((items) => items.map((item) => item.id === session.id ? { ...item, status } : item)); collegeApi.markAttendance(session.id, status) .then(() => { if (Platform.OS !== 'web') void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); }) .catch((error: Error) => { setClasses(previous); Alert.alert('Couldn’t save attendance', error.message); }); }; const removeClass = (session: Session) => { Alert.alert('Remove this class?', 'This removes only this occurrence. Your recurring timetable will not change.', [ { text: 'Keep class', style: 'cancel' }, { text: 'Remove', style: 'destructive', onPress: () => { setSelectedClass(null); setClasses((items) => items.filter((item) => item.id !== session.id)); collegeApi.removeClass(session.id).then(() => setRefresh((value) => value + 1)).catch((error: Error) => { Alert.alert('Couldn’t remove class', error.message); setRefresh((value) => value + 1); }); } }, ]); }; const isToday = selectedDateKey === todayKey; const insight = attendanceMessage(summary.percentage, summary.total); const insightTone = attendanceTone(summary.percentage, summary.total); const now = currentMinutes(); const recessEnabled = Boolean(profile && profile.recessEnabled && timeToMinutes(profile.recessStart) >= 0 && timeToMinutes(profile.recessStart) < timeToMinutes(profile.recessEnd)); const timelineItems = [ ...classes.map((item) => ({ type: 'class' as const, start: item.time, end: item.endTime, item })), ...(recessEnabled ? [{ type: 'recess' as const, start: profile!.recessStart, end: profile!.recessEnd }] : []), ].sort((left, right) => left.start.localeCompare(right.start) || (left.type === 'recess' ? -1 : 1)); const activeTimelineIndex = isToday ? timelineItems.findIndex((item) => timeToMinutes(item.start) <= now && now < timeToMinutes(item.end)) : -1; const insertionIndex = isToday ? (activeTimelineIndex >= 0 ? activeTimelineIndex : timelineItems.findIndex((item) => timeToMinutes(item.start) > now)) : -1; const currentTimeLabel = new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' }).format(new Date()); const pendingCount = classes.filter((item) => item.status === 'pending').length; return {!isToday ?