diff --git a/app.json b/app.json index e25f177..deb766f 100644 --- a/app.json +++ b/app.json @@ -2,6 +2,7 @@ "expo": { "name": "Study Sprint", "slug": "Study-Sprint", + "owner": "ikt205g26v-g18", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/images/icon.png", @@ -19,7 +20,8 @@ "monochromeImage": "./assets/images/android-icon-monochrome.png" }, "edgeToEdgeEnabled": true, - "predictiveBackGestureEnabled": false + "predictiveBackGestureEnabled": false, + "package": "com.teodorsa.StudySprint" }, "web": { "output": "static", @@ -44,6 +46,12 @@ "experiments": { "typedRoutes": true, "reactCompiler": true + }, + "extra": { + "router": {}, + "eas": { + "projectId": "2b2ec99b-a2ea-4991-8694-93f9e3d042a3" + } } } } diff --git a/app/(tabs)/assignments.tsx b/app/(tabs)/assignments.tsx index abeb58e..8fb9f77 100644 --- a/app/(tabs)/assignments.tsx +++ b/app/(tabs)/assignments.tsx @@ -1,5 +1,7 @@ import { defaultStyles } from '@/constants/defaultStyles'; +import { CheckSubjectCompletion } from '@/lib/progress'; import { supabase } from '@/lib/supabase'; +import type { Assignment, Task } from '@/lib/types'; import { Ionicons } from '@expo/vector-icons'; import { Session } from '@supabase/supabase-js'; import { router, Stack, useFocusEffect } from 'expo-router'; @@ -12,19 +14,9 @@ import { View, } from 'react-native'; -type Assignment = { - aId: string; - title: string; - description: string; - deadline: string; - isCompleted: boolean; - lastChanged: string; - uId: string; - sId: string; -}; - export default function Assignments() { const [assignments, SetAssignments] = useState([]); + const [tasksByAssignment, SetTasksByAssignment] = useState>({}); const [session, SetSession] = useState(null); const assignmentSections = [ @@ -55,17 +47,47 @@ export default function Assignments() { }, []); const GetAssignments = async () => { - const { data, error } = await supabase + const { data: assignmentsData, error: assignmentsError } = await supabase .from('assignments') .select('*') .order('deadline', { ascending: false }); - if (error) { + if (assignmentsError) { Alert.alert('Assignments could not be fetched, please try again'); return; } - SetAssignments(data ?? []); + const assignmentRows = assignmentsData ?? []; + SetAssignments(assignmentRows); + + if (assignmentRows.length === 0) { + SetTasksByAssignment({}); + return; + } + + const aIds = assignmentRows.map((assignment) => assignment.aId); + + const { data: tasksData, error: tasksError } = await supabase + .from('tasks') + .select('*') + .in('aId', aIds); + + if (tasksError) { + Alert.alert('Assignment tasks could not be fetched, please try again'); + SetTasksByAssignment({}); + return; + } + + const groupedTasks: Record = {}; + + for (const task of tasksData ?? []) { + if (!groupedTasks[task.aId]) { + groupedTasks[task.aId] = []; + } + groupedTasks[task.aId].push(task); + } + + SetTasksByAssignment(groupedTasks); }; useFocusEffect( @@ -76,7 +98,7 @@ export default function Assignments() { }, [session]) ); - const DeleteAssignment = async (aId: string) => { + const DeleteAssignment = async (aId: string, sId: string) => { Alert.alert( 'Delete Assignment', 'Are you sure you want to delete this assignment?', @@ -100,6 +122,13 @@ export default function Assignments() { } Alert.alert('Assignment deleted successfully!'); + + try { + await CheckSubjectCompletion(sId); + } catch { + Alert.alert("Failed to update subject status"); + } + GetAssignments(); }, }, @@ -178,6 +207,9 @@ export default function Assignments() { renderItem={({ item }) => { const isOwner = session?.user.id === item.uId; + const assignmentTasks = tasksByAssignment[item.aId] ?? []; + const progress = assignmentTasks.length === 0 ? 0 : Math.round((assignmentTasks.filter(task => task.isCompleted).length / assignmentTasks.length) * 100); + return ( + + + {progress}% + + + + + @@ -250,7 +304,7 @@ export default function Assignments() { DeleteAssignment(item.aId)} + onPress={() => DeleteAssignment(item.aId, item.sId)} > Delete diff --git a/app/(tabs)/subjects.tsx b/app/(tabs)/subjects.tsx index 5abaa9f..f5f5f79 100644 --- a/app/(tabs)/subjects.tsx +++ b/app/(tabs)/subjects.tsx @@ -1,5 +1,6 @@ import { defaultStyles } from '@/constants/defaultStyles'; import { supabase } from '@/lib/supabase'; +import type { Assignment, Subject } from '@/lib/types'; import { Ionicons } from '@expo/vector-icons'; import { Session } from '@supabase/supabase-js'; import { router, Stack, useFocusEffect } from 'expo-router'; @@ -12,17 +13,9 @@ import { View, } from 'react-native'; -type Subject = { - sId: string; - title: string; - description: string; - isActive: boolean; - lastChanged: string; - uId: string; -}; - export default function Subjects() { const [subjects, SetSubjects] = useState([]); + const [assignmentsBySubject, SetAssignmentsBySubject] = useState>({}); const [session, SetSession] = useState(null); const subjectSections = [ @@ -53,17 +46,47 @@ export default function Subjects() { }, []); const GetSubjects = async () => { - const { data, error } = await supabase + const { data: subjectsData, error: subjectsError } = await supabase .from('subjects') .select('*') .order('lastChanged', { ascending: false }); - if (error) { + if (subjectsError) { Alert.alert('Subjects could not be fetched, please try again'); return; } - SetSubjects(data ?? []); + const subjectRows = subjectsData ?? []; + SetSubjects(subjectsData ?? []); + + if (subjectRows.length === 0) { + SetAssignmentsBySubject({}); + return; + } + + const sIds = subjectRows.map((subject) => subject.sId); + + const { data: assignmentsData, error: assignmentsError } = await supabase + .from('assignments') + .select('*') + .in('sId', sIds); + + if (assignmentsError) { + Alert.alert('Subject assignments could not be fetched, please try again'); + SetAssignmentsBySubject({}); + return; + } + + const groupedAssignments: Record = {}; + + for (const assignment of assignmentsData ?? []) { + if (!groupedAssignments[assignment.sId]) { + groupedAssignments[assignment.sId] = []; + } + groupedAssignments[assignment.sId].push(assignment); + } + + SetAssignmentsBySubject(groupedAssignments); }; useFocusEffect( @@ -177,6 +200,9 @@ export default function Subjects() { renderItem={({ item }) => { const isOwner = session?.user.id === item.uId; + const subjectAssignments = assignmentsBySubject[item.sId] ?? []; + const progress = subjectAssignments.length === 0 ? 0 : Math.round((subjectAssignments.filter(assignment => assignment.isCompleted).length / subjectAssignments.length) * 100); + return ( + + {progress}% + + + + + diff --git a/app/(tabs)/tasks.tsx b/app/(tabs)/tasks.tsx index 447c948..7e08755 100644 --- a/app/(tabs)/tasks.tsx +++ b/app/(tabs)/tasks.tsx @@ -1,5 +1,7 @@ import { defaultStyles } from '@/constants/defaultStyles'; +import { CheckAssignmentCompletion } from '@/lib/progress'; import { supabase } from '@/lib/supabase'; +import type { Task } from '@/lib/types'; import { Ionicons } from '@expo/vector-icons'; import { Session } from '@supabase/supabase-js'; import { router, Stack, useFocusEffect } from 'expo-router'; @@ -12,16 +14,6 @@ import { View, } from 'react-native'; -type Task = { - tId: string; - title: string; - description: string; - isCompleted: boolean; - lastChanged: string; - uId: string; - aId: string; -}; - export default function Tasks() { const [tasks, SetTasks] = useState([]); const [session, SetSession] = useState(null); @@ -72,7 +64,7 @@ export default function Tasks() { }, [session]) ); - const DeleteTask = async (tId: string) => { + const DeleteTask = async (tId: string, aId: string) => { Alert.alert( 'Delete Task', 'Are you sure you want to delete this task?', @@ -96,6 +88,13 @@ export default function Tasks() { } Alert.alert('Task deleted successfully!'); + + try { + await CheckAssignmentCompletion(aId); + } catch { + Alert.alert("Failed to update assignment completion state"); + } + GetTasks(); }, }, @@ -246,7 +245,7 @@ export default function Tasks() { DeleteTask(item.tId)} + onPress={() => DeleteTask(item.tId, item.aId)} > Delete diff --git a/app/assignment/createAssignment.tsx b/app/assignment/createAssignment.tsx index 68be4c6..f377cb0 100644 --- a/app/assignment/createAssignment.tsx +++ b/app/assignment/createAssignment.tsx @@ -1,5 +1,6 @@ import { defaultStyles } from '@/constants/defaultStyles'; import * as AsyncStorage from '@/lib/asyncStorage'; +import { CheckSubjectCompletion } from '@/lib/progress'; import { supabase } from '@/lib/supabase'; import * as Notifications from 'expo-notifications'; import { router, Stack, useLocalSearchParams } from 'expo-router'; @@ -93,6 +94,14 @@ export default function CreateAssignment() { } } + if (sId) { + try { + await CheckSubjectCompletion(sId); + } catch { + Alert.alert("Failed to update subject status"); + } + } + SetTitle(''); SetDescription(''); SetDeadline(''); diff --git a/app/assignment/editAssignment.tsx b/app/assignment/editAssignment.tsx index 41fa9c3..7115942 100644 --- a/app/assignment/editAssignment.tsx +++ b/app/assignment/editAssignment.tsx @@ -1,22 +1,13 @@ import { defaultStyles } from '@/constants/defaultStyles'; import { GetAssignmentNotificationId, RemoveAssignmentNotificationId, SaveAssignmentNotificationId } from '@/lib/asyncStorage'; +import { CheckSubjectCompletion } from '@/lib/progress'; import { supabase } from '@/lib/supabase'; +import type { Assignment } from '@/lib/types'; import * as Notifications from 'expo-notifications'; import { router, Stack, useFocusEffect, useLocalSearchParams } from 'expo-router'; import { useCallback, useState } from 'react'; import { ActivityIndicator, Alert, Button, Keyboard, KeyboardAvoidingView, Platform, Pressable, Text, TextInput, TouchableWithoutFeedback, View } from 'react-native'; -type Assignment = { - aId: string; - title: string; - description: string; - deadline: string; - isCompleted: boolean; - lastChanged: string; - uId: string; - sId: string; -} - export default function EditAssignment() { const { aId } = useLocalSearchParams<{ aId: string }>(); const [assignment, SetAssignment] = useState(null) @@ -125,6 +116,14 @@ export default function EditAssignment() { } } + if (assignmentData.sId) { + try { + await CheckSubjectCompletion(assignmentData.sId); + } catch { + Alert.alert("Failed to update subject status"); + } + } + router.back(); } diff --git a/app/assignment/viewDetailsAssignment.tsx b/app/assignment/viewDetailsAssignment.tsx index e1548a7..e7dc364 100644 --- a/app/assignment/viewDetailsAssignment.tsx +++ b/app/assignment/viewDetailsAssignment.tsx @@ -1,31 +1,12 @@ import { defaultStyles } from '@/constants/defaultStyles'; +import { CheckAssignmentCompletion, CheckSubjectCompletion } from '@/lib/progress'; import { supabase } from '@/lib/supabase'; +import type { Assignment, Task } from '@/lib/types'; import { Session } from '@supabase/supabase-js'; import { router, Stack, useFocusEffect, useLocalSearchParams } from 'expo-router'; import { useCallback, useEffect, useState } from 'react'; import { Alert, Button, Pressable, SectionList, Text, View } from "react-native"; -type Assignment = { - aId: string; - title: string; - description: string; - deadline: string; - isCompleted: boolean; - lastChanged: string; - uId: string; - sId: string; -} - -type Task = { - tId: string; - title: string; - description: string; - isCompleted: boolean; - lastChanged: string; - uId: string; - aId: string; -} - export default function ViewDetailsAssignment() { const { aId } = useLocalSearchParams<{ aId: string }>(); const [assignment, SetAssignment] = useState(null) @@ -98,6 +79,17 @@ export default function ViewDetailsAssignment() { } Alert.alert("Assignment deleted successfully!"); + + const sId = assignment?.sId; + + if (sId) { + try { + await CheckSubjectCompletion(sId); + } catch { + Alert.alert("Failed to update subject status"); + } + } + router.back(); } } @@ -126,6 +118,15 @@ export default function ViewDetailsAssignment() { } Alert.alert("Task deleted successfully!"); + + if (aId) { + try { + await CheckAssignmentCompletion(aId); + } catch { + Alert.alert("Failed to update assignment completion state"); + } + } + GetTasks(aId); } } @@ -133,6 +134,8 @@ export default function ViewDetailsAssignment() { ) } + const progress = tasks.length === 0 ? 0 : Math.round((tasks.filter(task => task.isCompleted).length / tasks.length) * 100); + return ( } {assignment.lastChanged} + + {progress}% + + + + +