import { GetActiveSession, RemoveActiveSession, type ActiveSession, } from '@/lib/asyncStorage'; import type { SessionType } from '@/lib/types'; import { formatDate, formatDateTime } from '@/lib/date'; import { RegisterForLocalNotificationsAsync } from '@/lib/notifications'; import { CheckAssignmentCompletion } from '@/lib/progress'; import { supabase } from "@/lib/supabase"; import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import { Session } from '@supabase/supabase-js'; import { router, Stack, useFocusEffect } from "expo-router"; import { useCallback, useEffect, useState } from 'react'; import { Alert, Modal, Pressable, ScrollView, Text, View, } from "react-native"; type UpcomingDeadlineTask = { tId: string; title: string; description: string; aId: string; subjectTitle: string; assignmentTitle: string; deadline: string; }; type DashboardProgressSummary = { completedFocusSessionsToday: number; minutesStudiedToday: number; minutesStudiedThisWeek: number; }; type RecentSession = { sessionId: string; taskTitle: string | null; sessionType: SessionType; elapsedSeconds: number; status: string; startedAt: string | null; endedAt: string | null; }; type RecentlyCompletedTask = { tId: string; title: string; assignmentTitle: string; lastChanged: string; }; const FLOW_STEPS = [ { label: '1', title: 'Subject', description: 'A subject is the top-level container for one course or area you are studying.', }, { label: '2', title: 'Assignment', description: 'Each subject contains assignments like projects, exercises, or exam prep blocks.', }, { label: '3', title: 'Task', description: 'Assignments are broken down into tasks so you always have one concrete thing to work on.', }, { label: '4', title: 'Sprint', description: 'A sprint is one focused work session tied to a single task and tracked by the timer.', }, ] as const; function formatTime(totalSeconds: number) { const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; } function getSessionLabel(sessionType: SessionType) { switch (sessionType) { case 'short_break': return 'Short Break'; case 'long_break': return 'Long Break'; default: return 'Active Sprint'; } } function formatTrackedMinutes(totalSeconds: number) { return Math.floor(totalSeconds / 60); } function formatTrackedDuration(totalSeconds: number) { if (totalSeconds <= 0) { return '0m'; } const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); if (hours === 0) { return `${minutes}m`; } if (minutes === 0) { return `${hours}h`; } return `${hours}h ${minutes}m`; } function getSessionStatusLabel(status: string) { switch (status) { case 'completed': return 'Completed'; case 'cancelled': return 'Cancelled'; case 'expired': return 'Timed out'; default: return status; } } function getStartOfToday() { const now = new Date(); return new Date(now.getFullYear(), now.getMonth(), now.getDate()); } function getStartOfWeek() { const today = getStartOfToday(); const currentDay = today.getDay(); const daysSinceMonday = (currentDay + 6) % 7; const startOfWeek = new Date(today); startOfWeek.setDate(today.getDate() - daysSinceMonday); return startOfWeek; } export default function HomeScreen() { const [session, SetSession] = useState(null); const [activeSprint, setActiveSprint] = useState(null); const [activeSprintTaskTitle, setActiveSprintTaskTitle] = useState(null); const [activeSprintTaskDesc, setActiveSprintTaskDesc] = useState(null); const [remainingSeconds, setRemainingSeconds] = useState(0); const [dashboardSummary, setDashboardSummary] = useState({ completedFocusSessionsToday: 0, minutesStudiedToday: 0, minutesStudiedThisWeek: 0, }); const [recentSessions, setRecentSessions] = useState([]); const [recentlyCompletedTasks, setRecentlyCompletedTasks] = useState([]); const [upcomingDeadlineTasks, setUpcomingDeadlineTasks] = useState([]); const [isFlowInfoVisible, setIsFlowInfoVisible] = useState(false); const [completingTaskId, setCompletingTaskId] = useState(null); const [subjectCount, setSubjectCount] = useState(0); const loadActiveSprint = useCallback(async () => { const storedSprint = await GetActiveSession(); if (!storedSprint) { setActiveSprint(null); setActiveSprintTaskTitle(null); setActiveSprintTaskDesc(null); setRemainingSeconds(0); return; } const secondsLeft = Math.max( 0, Math.ceil((storedSprint.endTime - Date.now()) / 1000) ); if (secondsLeft <= 0) { await RemoveActiveSession(); setActiveSprint(null); setActiveSprintTaskTitle(null); setActiveSprintTaskDesc(null); setRemainingSeconds(0); return; } setActiveSprint(storedSprint); setRemainingSeconds(secondsLeft); if (!storedSprint.taskId) { setActiveSprintTaskTitle(getSessionLabel(storedSprint.sessionType)); setActiveSprintTaskDesc('Take the break before you jump into the next focus session.'); return; } const { data: dbTitle } = await supabase .from('tasks') .select('title') .eq('tId', storedSprint.taskId) .single(); const { data: dbDesc } = await supabase .from('tasks') .select('description') .eq('tId', storedSprint.taskId) .single(); setActiveSprintTaskTitle(dbTitle?.title ?? null); setActiveSprintTaskDesc(dbDesc?.description); }, []); const loadUpcomingDeadlineTasks = useCallback(async () => { if (!session?.user.id) { setUpcomingDeadlineTasks([]); return; } const { data: tasksData, error: tasksError } = await supabase .from('tasks') .select('tId, title, description, aId') .eq('uId', session.user.id) .eq('isCompleted', false); if (tasksError || !tasksData || tasksData.length === 0) { setUpcomingDeadlineTasks([]); return; } const assignmentIds = [...new Set(tasksData.map((task) => task.aId).filter(Boolean))]; if (assignmentIds.length === 0) { setUpcomingDeadlineTasks([]); return; } const { data: assignmentsData, error: assignmentsError } = await supabase .from('assignments') .select('aId, sId, title, deadline') .in('aId', assignmentIds) .eq('isCompleted', false); if (assignmentsError || !assignmentsData) { setUpcomingDeadlineTasks([]); return; } const subjectIds = [...new Set(assignmentsData.map((assignment) => assignment.sId).filter(Boolean))]; const { data: subjectsData, error: subjectsError } = await supabase .from('subjects') .select('sId, title') .in('sId', subjectIds); if (subjectsError || !subjectsData) { setUpcomingDeadlineTasks([]); return; } const now = Date.now(); const assignmentsById = new Map( assignmentsData.map((assignment) => [assignment.aId, assignment]) ); const subjectsById = new Map( subjectsData.map((subject) => [subject.sId, subject]) ); const enrichedTasks = tasksData .map((task) => { const assignment = assignmentsById.get(task.aId); if (!assignment?.deadline) { return null; } const deadlineTime = new Date(assignment.deadline).getTime(); if (Number.isNaN(deadlineTime) || deadlineTime < now) { return null; } const subject = subjectsById.get(assignment.sId); return { tId: task.tId, title: task.title, description: task.description, aId: task.aId, subjectTitle: subject?.title ?? 'Unknown Subject', assignmentTitle: assignment.title, deadline: assignment.deadline, } satisfies UpcomingDeadlineTask; }) .filter((task): task is UpcomingDeadlineTask => task !== null) .sort( (left, right) => new Date(left.deadline).getTime() - new Date(right.deadline).getTime() ); setUpcomingDeadlineTasks(enrichedTasks); }, [session?.user.id]); const loadDashboardProgress = useCallback(async () => { if (!session?.user.id) { setDashboardSummary({ completedFocusSessionsToday: 0, minutesStudiedToday: 0, minutesStudiedThisWeek: 0, }); setRecentSessions([]); setRecentlyCompletedTasks([]); return; } const startOfToday = getStartOfToday().toISOString(); const startOfWeek = getStartOfWeek().toISOString(); const [ { data: weeklySessions, error: weeklySessionsError }, { data: rawRecentSessions, error: recentSessionsError }, { data: completedTasks, error: completedTasksError }, { count: fetchedSubjectCount, error: subjectCountError }, ] = await Promise.all([ supabase .from('sprint_sessions') .select('sessionId, sessionType, elapsedSeconds, status, startedAt, endedAt') .eq('userId', session.user.id) .eq('sessionType', 'focus') .not('endedAt', 'is', null) .gte('endedAt', startOfWeek), supabase .from('sprint_sessions') .select('sessionId, taskId, sessionType, elapsedSeconds, status, startedAt, endedAt') .eq('userId', session.user.id) .not('endedAt', 'is', null) .order('endedAt', { ascending: false }) .limit(6), supabase .from('tasks') .select('tId, title, aId, lastChanged') .eq('uId', session.user.id) .eq('isCompleted', true) .order('lastChanged', { ascending: false }) .limit(3), supabase .from('subjects') .select('sId', { count: 'exact', head: true }) .eq('uId', session.user.id), ]); if (weeklySessionsError || recentSessionsError || completedTasksError || subjectCountError) { setDashboardSummary({ completedFocusSessionsToday: 0, minutesStudiedToday: 0, minutesStudiedThisWeek: 0, }); setRecentSessions([]); setRecentlyCompletedTasks([]); setSubjectCount(0); return; } setSubjectCount(fetchedSubjectCount ?? 0); const weeklySessionRows = weeklySessions ?? []; const todaySummary = weeklySessionRows.reduce( (summary, currentSession) => { const endedAt = currentSession.endedAt ? new Date(currentSession.endedAt) : null; if (!endedAt || Number.isNaN(endedAt.getTime())) { return summary; } const elapsedSeconds = currentSession.elapsedSeconds ?? 0; summary.minutesStudiedThisWeek += formatTrackedMinutes(elapsedSeconds); if (endedAt >= new Date(startOfToday)) { summary.minutesStudiedToday += formatTrackedMinutes(elapsedSeconds); if (currentSession.status === 'completed') { summary.completedFocusSessionsToday += 1; } } return summary; }, { completedFocusSessionsToday: 0, minutesStudiedToday: 0, minutesStudiedThisWeek: 0, } satisfies DashboardProgressSummary ); setDashboardSummary(todaySummary); const recentSessionRows = rawRecentSessions ?? []; const recentTaskIds = [ ...new Set( recentSessionRows .map((recentSession) => recentSession.taskId) .filter((taskId): taskId is string => Boolean(taskId)) ), ]; const completedTaskRows = completedTasks ?? []; const completedAssignmentIds = [ ...new Set( completedTaskRows .map((task) => task.aId) .filter((assignmentId): assignmentId is string => Boolean(assignmentId)) ), ]; const [{ data: recentTasks }, { data: completedAssignments }] = await Promise.all([ recentTaskIds.length > 0 ? supabase .from('tasks') .select('tId, title') .in('tId', recentTaskIds) : Promise.resolve({ data: [], error: null }), completedAssignmentIds.length > 0 ? supabase .from('assignments') .select('aId, title') .in('aId', completedAssignmentIds) : Promise.resolve({ data: [], error: null }), ]); const tasksById = new Map((recentTasks ?? []).map((task) => [task.tId, task.title])); const assignmentsById = new Map( (completedAssignments ?? []).map((assignment) => [assignment.aId, assignment.title]) ); setRecentSessions( recentSessionRows.map((recentSession) => ({ sessionId: recentSession.sessionId, taskTitle: recentSession.taskId ? (tasksById.get(recentSession.taskId) ?? null) : null, sessionType: recentSession.sessionType, elapsedSeconds: recentSession.elapsedSeconds ?? 0, status: recentSession.status, startedAt: recentSession.startedAt, endedAt: recentSession.endedAt, })) ); setRecentlyCompletedTasks( completedTaskRows.map((task) => ({ tId: task.tId, title: task.title, assignmentTitle: assignmentsById.get(task.aId) ?? 'Unknown Assignment', lastChanged: task.lastChanged, })) ); }, [session?.user.id]); useEffect(() => { supabase.auth .getSession() .then(({ data }) => SetSession(data.session ?? null)); const { data: sub } = supabase.auth.onAuthStateChange( (_event, newSession) => { SetSession(newSession); } ); return () => sub.subscription.unsubscribe(); }, []); useEffect(() => { if (session) { RegisterForLocalNotificationsAsync(); } }, [session]); useFocusEffect( useCallback(() => { void loadActiveSprint(); void loadDashboardProgress(); void loadUpcomingDeadlineTasks(); }, [loadActiveSprint, loadDashboardProgress, loadUpcomingDeadlineTasks]) ); useEffect(() => { if (!activeSprint) { return; } const intervalId = setInterval(() => { const secondsLeft = Math.max( 0, Math.ceil((activeSprint.endTime - Date.now()) / 1000) ); setRemainingSeconds(secondsLeft); if (secondsLeft <= 0) { void RemoveActiveSession(); setActiveSprint(null); setActiveSprintTaskTitle(null); setActiveSprintTaskDesc(null); } }, 1000); return () => clearInterval(intervalId); }, [activeSprint]); const handleTaskCompletion = useCallback(async (task: UpcomingDeadlineTask) => { if (completingTaskId) { return; } setCompletingTaskId(task.tId); const { error } = await supabase .from('tasks') .update({ isCompleted: true, lastChanged: new Date().toISOString(), }) .eq('tId', task.tId); if (error) { setCompletingTaskId(null); Alert.alert('Task could not be completed, please try again'); return; } try { await CheckAssignmentCompletion(task.aId); } catch { setCompletingTaskId(null); Alert.alert('Task was updated, but assignment progress could not be refreshed'); return; } setUpcomingDeadlineTasks((currentTasks) => currentTasks.filter((currentTask) => currentTask.tId !== task.tId) ); setCompletingTaskId(null); }, [completingTaskId]); return ( { return ( setIsFlowInfoVisible(true)} > ) }, headerRight: () => { return ( await supabase.auth.signOut()} > Logout ) }, }} /> setIsFlowInfoVisible(false)} > setIsFlowInfoVisible(false)} /> How the app is structured Study flow setIsFlowInfoVisible(false)} > Build your work from the big container down to the focused work session. {FLOW_STEPS.map((step, index) => ( {step.label} {index < FLOW_STEPS.length - 1 ? ( ) : null} {step.title} {step.description} ))} Quick map {'Subject -> Assignment -> Task -> Sprint'} { setIsFlowInfoVisible(false); router.push('/subjects'); }} > Start with Subjects {subjectCount === 0 ? ( First step Build your first study path Start with one subject, then add one assignment and one task so you can reach your first sprint without guessing what to do next. router.push('/setup')} > Start Guided Setup ) : null} {activeSprint ? ( {getSessionLabel(activeSprint.sessionType)} {activeSprintTaskTitle ?? 'Selected task'} {' '} {activeSprintTaskDesc ?? null} {' '} {formatTime(remainingSeconds)} remaining router.push({ pathname: '/task/timer', params: activeSprint.taskId ? { tId: activeSprint.taskId } : { sessionType: activeSprint.sessionType, durationMinutes: String(Math.max(1, Math.round(activeSprint.durationSeconds / 60))), }, }) } > Open Session ) : ( No active sprint right now. )} Study progress A quick view of today's and this week's focused study effort. Focus sessions today {dashboardSummary.completedFocusSessionsToday} Minutes today {dashboardSummary.minutesStudiedToday} Minutes this week {dashboardSummary.minutesStudiedThisWeek} Tasks with upcoming deadlines The next concrete work items that are most likely to matter soon. {upcomingDeadlineTasks.length > 0 ? ( upcomingDeadlineTasks.map((task) => ( router.push({ pathname: '/task/viewDetailsTask', params: { tId: task.tId }, }) } > {task.title} {task.description ? ( {task.description} ) : null} {task.subjectTitle} • {task.assignmentTitle} • {formatDate(task.deadline)} { event.stopPropagation(); Alert.alert( 'Complete task', 'Mark this task as completed?', [ { text: 'Cancel', style: 'cancel', }, { text: 'Complete', onPress: () => { void handleTaskCompletion(task); }, }, ] ); }} > {completingTaskId === task.tId ? 'Completing...' : 'Mark as completed'} )) ) : ( No upcoming task deadlines. )} Recent sessions The latest recorded sprints and breaks. {recentSessions.length > 0 ? ( recentSessions.map((recentSession) => ( {recentSession.taskTitle ?? getSessionLabel(recentSession.sessionType)} {getSessionLabel(recentSession.sessionType)} • {formatTrackedDuration(recentSession.elapsedSeconds)} {getSessionStatusLabel(recentSession.status)} {formatDateTime(recentSession.endedAt ?? recentSession.startedAt)} )) ) : ( No recent sessions yet. )} Recently completed tasks Tasks you have recently finished and moved out of the queue. {recentlyCompletedTasks.length > 0 ? ( recentlyCompletedTasks.map((task) => ( router.push({ pathname: '/task/viewDetailsTask', params: { tId: task.tId }, }) } > {task.title} {task.assignmentTitle} Completed {formatDateTime(task.lastChanged)} )) ) : ( No completed tasks yet. )} ); }