import { GetActiveSession, 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 { DEFAULT_FOCUS_DURATION_MINUTES } from '@/lib/sessionDefaults'; import { getSetupStatus } from '@/lib/setupStatus'; import { finalizeStoredSession } from '@/lib/sessionLifecycle'; import { supabase } from "@/lib/supabase"; import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import { Session } from '@supabase/supabase-js'; import { Redirect, 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. After it ends, you can take a break, continue the same task, or return to the dashboard.', }, ] 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 [needsSetup, setNeedsSetup] = useState(null); 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 finalizeStoredSession('expired', storedSprint); 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]); useEffect(() => { const loadSetupGate = async () => { if (!session?.user.id) { setNeedsSetup(false); return; } try { const setupStatus = await getSetupStatus(session.user.id); setNeedsSetup(!setupStatus.isSetupComplete); } catch { setNeedsSetup(true); } }; setNeedsSetup(null); void loadSetupGate(); }, [session?.user.id]); 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 finalizeStoredSession('expired', activeSprint); 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]); const handleStartSprint = useCallback(async (task: UpcomingDeadlineTask) => { const storedSession = await GetActiveSession(); if (!storedSession) { router.push({ pathname: '/task/timer', params: { tId: task.tId, durationMinutes: String(DEFAULT_FOCUS_DURATION_MINUTES), }, }); return; } const secondsLeft = Math.ceil((storedSession.endTime - Date.now()) / 1000); if (secondsLeft <= 0) { await finalizeStoredSession('expired', storedSession); router.push({ pathname: '/task/timer', params: { tId: task.tId, durationMinutes: String(DEFAULT_FOCUS_DURATION_MINUTES), }, }); return; } if (storedSession.taskId === task.tId) { router.push({ pathname: '/task/timer', params: { tId: task.tId, durationMinutes: String(DEFAULT_FOCUS_DURATION_MINUTES), }, }); return; } Alert.alert( 'Active session in progress', `End the current session and start a new ${DEFAULT_FOCUS_DURATION_MINUTES} minute sprint on "${task.title}"?`, [ { text: 'Cancel', style: 'cancel', }, { text: 'Start sprint', style: 'destructive', onPress: async () => { await finalizeStoredSession('cancelled', storedSession); router.push({ pathname: '/task/timer', params: { tId: task.tId, durationMinutes: String(DEFAULT_FOCUS_DURATION_MINUTES), }, }); }, }, ] ); }, []); if (session && needsSetup === null) { return null; } if (needsSetup) { return ; } return ( { return ( setIsFlowInfoVisible(true)} > ) }, headerRight: () => { return ( await supabase.auth.signOut()} > Logout ) }, }} /> setIsFlowInfoVisible(false)} > setIsFlowInfoVisible(false)} /> How work is organized Study flow setIsFlowInfoVisible(false)} > Build your work from the big container down to one concrete task, then use sprints and breaks to move that work forward. {FLOW_STEPS.map((step, index) => ( {step.label} {index < FLOW_STEPS.length - 1 ? ( ) : null} {step.title} {step.description} ))} Quick map {'Subject -> Assignment -> Task -> Sprint'} The dashboard then helps you resume an active session, start the next sprint, or review recent study progress. { setIsFlowInfoVisible(false); router.push('/subjects'); }} > Open 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))), }, }) } > {activeSprint.sessionType === 'focus' ? 'Resume Sprint' : 'Resume Break'} ) : ( 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(); void handleStartSprint(task); }} > Start Sprint { 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. )} ); }