diff --git a/client-new/App.tsx b/client-new/App.tsx index d0f5f36..e707288 100644 --- a/client-new/App.tsx +++ b/client-new/App.tsx @@ -15,6 +15,11 @@ import { NativeBaseProvider, extendTheme } from 'native-base'; import React from 'react'; import { StyleSheet } from 'react-native'; import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { LogBox } from 'react-native'; + +// Ignore log notification by message: +// LogBox.ignoreLogs(['Warning: ...']); // Ignore log notification by message +// LogBox.ignoreAllLogs();//Ignore all log notifications const queryClient = new QueryClient({ defaultOptions: { diff --git a/client-new/src/components/homescreen components/HomeScreenTaskCard.tsx b/client-new/src/components/homescreen components/HomeScreenTaskCard.tsx index 27a6124..001ecfa 100644 --- a/client-new/src/components/homescreen components/HomeScreenTaskCard.tsx +++ b/client-new/src/components/homescreen components/HomeScreenTaskCard.tsx @@ -1,12 +1,21 @@ import { ITask } from '@/interfaces/ITask'; -import { fetchTaskTag } from '@/services/TaskService'; +import { fetchTaskTag, getTaskProgress } from '@/services/TaskService'; import { Text, View } from 'native-base'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { Pressable, TouchableOpacity } from 'react-native'; import RightArrowIcon from '../icons/RightArrowIcon'; import CircleProgress from '../reusable/CircleProgress'; +import { useQuery } from '@tanstack/react-query'; +import { useUser } from '@/contexts/UserContext'; +import { useIsFocused } from '@react-navigation/native'; +import { color } from 'native-base/lib/typescript/theme/styled-system'; +import { + heightPercentageToDP as h, + widthPercentageToDP as w +} from 'react-native-responsive-screen'; + type HSTCProps = { task: ITask; @@ -15,10 +24,28 @@ type HSTCProps = { }; const HomeScreenTaskCard: React.FC = ({ task, isAllTasks, handleOnPress }) => { + const { user } = useUser(); + const isFocused = useIsFocused(); const [tag, setTag] = useState(null); const [isPending, setIsPending] = useState(false); const [error, setError] = useState(null); + const { isLoading, error: completeError, data: complete, refetch } = useQuery({ + queryKey: ['fetchTaskProgress', user?.id, task?.id], + queryFn: () => getTaskProgress(user?.id, task?.id) + }); + + const refreshData = useCallback(async () => { + await refetch(); + }, [refetch]); + + + useEffect(() => { + if (isFocused) { + refreshData(); + } + }, [isFocused, refetch]); + useEffect(() => { if (!isAllTasks) { const fetchData = async () => { @@ -37,10 +64,19 @@ const HomeScreenTaskCard: React.FC = ({ task, isAllTasks, handleOnPre } }, [isAllTasks, task.id]); - const progress = Math.floor(Math.random() * 100) + 1; + if (isLoading) { + return Loading... + }; + + if (completeError) { + return Error + }; return ( - + = ({ task, isAllTasks, handleOnPre fontWeight: '600', marginBottom: 5 }} + color={complete?.progress === 100 ? '#00000033' : '#2F1D12'} > {task.task_name} {task.task_description} - + ( +type RightArrowIconProps = { + color?: string; +}; + +const RightArrowIcon = ({ color }: RightArrowIconProps) => ( ); + export default RightArrowIcon; diff --git a/client-new/src/components/reusable/CircleProgress.tsx b/client-new/src/components/reusable/CircleProgress.tsx index 0170fac..f6148dd 100644 --- a/client-new/src/components/reusable/CircleProgress.tsx +++ b/client-new/src/components/reusable/CircleProgress.tsx @@ -1,31 +1,50 @@ -import React, { useEffect, useRef } from 'react'; -import { Animated, Text, View } from 'react-native'; +import { useUser } from '@/contexts/UserContext'; +import { ITask } from '@/interfaces/ITask'; +import { getTaskProgress } from '@/services/TaskService'; +import { useQuery } from '@tanstack/react-query'; +import React, { useEffect, useRef, useState } from 'react'; +import { Animated } from 'react-native'; import Svg, { Circle } from 'react-native-svg'; +import { Text, View } from 'native-base'; const AnimatedCircle = Animated.createAnimatedComponent(Circle); -const CircleProgress = ({ progress }) => { - const animatedValue = useRef(new Animated.Value(0)).current; +type CircleProgressProps = { + task: ITask; +}; + +const CircleProgress = ({ task }: CircleProgressProps) => { + const { user } = useUser(); + + const { isLoading, error, data: progress, refetch } = useQuery({ + queryKey: ['fetchTaskProgress', task?.id], + queryFn: () => getTaskProgress(user?.id, task?.id) + }); - const strokeWidth = 10; + const animatedValue = useRef(new Animated.Value(0)).current; + const strokeWidth = 13; const radius = 50 - strokeWidth / 2; const circumference = 2 * Math.PI * radius; - const progressStrokeDashoffset = ((progress / 100) * circumference) / 100; useEffect(() => { - Animated.timing(animatedValue, { - toValue: progress, - duration: 1000, - useNativeDriver: true - }).start(); - }, [animatedValue, progress]); + if (progress?.progress !== undefined) { + Animated.timing(animatedValue, { + toValue: progress.progress || 0, + duration: 1000, + useNativeDriver: true + }).start(); + } + }, [animatedValue, progress?.progress]); + + const progressStrokeDashoffset = animatedValue.interpolate({ + inputRange: [0, 100], + outputRange: [circumference, 0] + }); - const { left, top } = - progress < 10 - ? { left: 26, top: 27 } - : progress >= 100 - ? { left: 17, top: 27 } - : { left: 20, top: 27 }; + const textPosition = { + left: 43 - (progress?.progress < 10 ? 6 : 10), + top: 42 - (progress?.progress < 10 ? 6 : 10) + }; return ( @@ -45,26 +64,20 @@ const CircleProgress = ({ progress }) => { r={radius} stroke="#43A573" strokeWidth={strokeWidth} - strokeDasharray={circumference} - strokeDashoffset={animatedValue.interpolate({ - inputRange: [0, 100], - outputRange: [circumference, progressStrokeDashoffset] - })} + strokeDasharray={`${circumference} ${circumference}`} + strokeDashoffset={progressStrokeDashoffset} strokeLinecap="round" fill="none" transform="rotate(-90 50 50)" /> - {`${progress}%`} + {progress?.progress || 0}% ); diff --git a/client-new/src/components/reusable/CircleProgressSubtask.tsx b/client-new/src/components/reusable/CircleProgressSubtask.tsx index ab53976..75e5ed9 100644 --- a/client-new/src/components/reusable/CircleProgressSubtask.tsx +++ b/client-new/src/components/reusable/CircleProgressSubtask.tsx @@ -1,31 +1,66 @@ -import React, { useEffect, useRef } from 'react'; -import { Animated, Text, View } from 'react-native'; +import { useUser } from '@/contexts/UserContext'; +import { ITask } from '@/interfaces/ITask'; +import { getTaskProgress } from '@/services/TaskService'; +import { useQuery } from '@tanstack/react-query'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { Animated } from 'react-native'; import Svg, { Circle } from 'react-native-svg'; +import { Text, View } from 'native-base'; +import { useIsFocused } from '@react-navigation/native'; // Import useIsFocused hook const AnimatedCircle = Animated.createAnimatedComponent(Circle); -const CircleProgress = ({ progress }) => { - const animatedValue = useRef(new Animated.Value(0)).current; +type CircleProgressProps = { + task: ITask; +}; + +const CircleProgress = ({ task }: CircleProgressProps) => { + const { user } = useUser(); + const isFocused = useIsFocused(); // Hook to check if screen is focused + + const { isLoading, error, data: progress, refetch } = useQuery({ + queryKey: ['fetchTaskProgress', task?.id], + queryFn: () => getTaskProgress(user?.id, task?.id) + }); + + + const refreshData = useCallback(async () => { + console.log(progress) + await refetch(); + }, [refetch]); + + useEffect(() => { + if (isFocused) { + refreshData(); + } + }, [isFocused, refetch]); + + const animatedValue = useRef(new Animated.Value(0)).current; const strokeWidth = 13; const radius = 50 - strokeWidth / 2; const circumference = 2 * Math.PI * radius; - const progressStrokeDashoffset = ((progress / 100) * circumference) / 100; useEffect(() => { - Animated.timing(animatedValue, { - toValue: progress, - duration: 1000, - useNativeDriver: true - }).start(); - }, [animatedValue, progress]); + if (progress?.progress !== undefined) { + Animated.timing(animatedValue, { + toValue: progress.progress || 0, + duration: 1000, + useNativeDriver: true + }).start(); + } + }, [animatedValue, progress?.progress]); + + const progressStrokeDashoffset = animatedValue.interpolate({ + inputRange: [0, 100], + outputRange: [circumference, 0] + }); + + const textPosition = { + left: 55 - (progress?.progress < 10 ? 6 : 10), + top: 45 - (progress?.progress < 10 ? 6 : 10) + }; - const { left, top } = - progress < 10 - ? { left: 165, top: 41 } - : progress >= 100 - ? { left: 156, top: 41 } - : { left: 159, top: 41 }; return ( @@ -37,7 +72,7 @@ const CircleProgress = ({ progress }) => { strokeWidth={strokeWidth} strokeDasharray={circumference} strokeLinecap="round" - fill="transparent" + fill="transparent" /> { stroke="#43A573" strokeWidth={strokeWidth} strokeDasharray={circumference} - strokeDashoffset={animatedValue.interpolate({ - inputRange: [0, 100], - outputRange: [circumference, progressStrokeDashoffset] - })} + strokeDashoffset={progressStrokeDashoffset} strokeLinecap="round" fill="transparent" transform="rotate(-90 50 50)" /> - {`${progress}%`} + {progress?.progress || 0}% ); }; export default CircleProgress; - -//style={{ position: 'absolute', top: top, left: left, zIndex: 2, fontSize: 15 }} diff --git a/client-new/src/components/task/Actions.tsx b/client-new/src/components/task/Actions.tsx deleted file mode 100644 index c4fc5b6..0000000 --- a/client-new/src/components/task/Actions.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { useUser } from '@/contexts/UserContext'; -import { IAction } from '@/interfaces/IAction'; -import { createFile } from '@/services/CreateFileService'; -import { - Button, - Checkbox, - FormControl, - HStack, - Input, - Radio, - ScrollView, - Select, - Text, - TextArea, - View -} from 'native-base'; -import { heightPercentageToDP as h } from 'react-native-responsive-screen'; - -import React, { useCallback, useEffect, useState } from 'react'; -import { ZodIssue, z } from 'zod'; -import InputField from '@/components/task/InputField'; -import SelectField from '@/components/task/SelectField'; -import TextAreaField from '@/components/task/TextAreaField'; -import CheckboxField from './CheckboxField'; -import RadioField from './RadioField'; -import { GestureResponderEvent } from 'react-native'; - - -type FormComponentProps = { - actions: IAction[]; - subTaskName: string; -}; - -const FormComponent = ({ actions, subTaskName }: FormComponentProps) => { - const [formState, setFormState] = useState({}); - const [formErrors, setFormErrors] = useState([]); - const { user } = useUser(); - - const handleSubmit = async (e: GestureResponderEvent) => { - e.preventDefault(); - - - try { - setFormState({ ...formState, user_id: user?.id, sub_task_name: subTaskName, timestamp: Date.now() }) - await createFile(user?.id, subTaskName, formState); - } - catch (err) { - console.log(err); - } - } - - const renderField = (action, index: number) => { - switch (action.action_type) { - case 'input': - return - case 'select': - return - case 'textarea': - return - case 'checkbox': - return - case 'radio': - return - default: - return null; - } - }; - - return ( - - {actions.map((action, index) => ( - - error.path[0] === action.name)} - key={index} - mt={4} - > - - {action.label} - - {renderField(action, index)} - - - ))} - - - ); -} - -export default FormComponent; - -type SubmitButtonProps = { - handleSubmit: (e: GestureResponderEvent) => void -} - -const SubmitButton = ({ handleSubmit }: SubmitButtonProps) => { - return ( - - - - ) -} \ No newline at end of file diff --git a/client-new/src/components/task/InputField.tsx b/client-new/src/components/task/InputField.tsx index b104f5a..930f5a4 100644 --- a/client-new/src/components/task/InputField.tsx +++ b/client-new/src/components/task/InputField.tsx @@ -1,7 +1,7 @@ import { IInput } from "@/interfaces/IAction"; import { Input, View, Text } from "native-base"; import { ZodIssue, z } from "zod"; -import React from "react"; +import React, { useCallback } from "react"; import { heightPercentageToDP as h } from "react-native-responsive-screen"; type InputFieldProps = { @@ -15,16 +15,16 @@ type InputFieldProps = { const InputField: React.FC = (InputFieldProps) => { const { action, index, setFormState, setFormErrors, formErrors } = InputFieldProps; - const handleInputChange = (name: string, value: string) => { + const handleInputChange = useCallback((name: string, value: string) => { const errorMessage = validateInput(value); setFormState((prevState) => ({ ...prevState, [name]: value })); setFormErrors((prevErrors) => ({ ...prevErrors, [name]: errorMessage })); - }; + }, []); const validateInput = (value: string) => { try { - const schema = z.string().min(1).max(20); + const schema = z.string().min(1).max(50); schema.parse(value); return undefined; } catch (error) { diff --git a/client-new/src/components/task/SubTaskCard.tsx b/client-new/src/components/task/SubTaskCard.tsx new file mode 100644 index 0000000..58ad4ca --- /dev/null +++ b/client-new/src/components/task/SubTaskCard.tsx @@ -0,0 +1,99 @@ +import React, { useCallback, useEffect } from 'react' +import { View, Text, Pressable, Button } from 'native-base' +import { moderateScale } from '@/utils/FontSizeUtils' +import { heightPercentageToDP as h, widthPercentageToDP as w } from 'react-native-responsive-screen' +import RightArrowIcon from '@/components/icons/RightArrowIcon' +import { ISubTask } from '@/interfaces/ISubTask' +import { isLoading } from 'expo-font'; +import { useQuery } from '@tanstack/react-query' +import { getSubtaskProgress } from '@/services/SubTasksService' +import { useUser } from '@/contexts/UserContext' +import { useIsFocused } from '@react-navigation/native'; // Import useIsFocused hook + +type SubTasksProps = { + subtask: ISubTask + navigation: any +} + +const SubTaskCard = ({ subtask, navigation }: SubTasksProps) => { + const { user } = useUser(); + const isFocused = useIsFocused(); + + const { isLoading, error, data: complete, refetch } = useQuery({ + queryKey: ['fetchSubtaskProgress', user?.id, subtask?.id], + queryFn: () => getSubtaskProgress(user?.id, subtask?.id) + }); + + const refreshData = useCallback(async () => { + await refetch(); + }, [refetch]); + + + useEffect(() => { + if (isFocused) { + refreshData(); + } + }, [isFocused, refetch]); + + if (isLoading) { + return Loading... + }; + + if (error) { + return Error + }; + + return ( + navigation.navigate('Action Screen', { subtask: subtask })} + isDisabled={complete?.completed} + > + + + + + {subtask?.sub_task_name} + + + {subtask?.sub_task_description} + + + + + + + + + ) +} + +export default SubTaskCard; \ No newline at end of file diff --git a/client-new/src/components/task/Subtask.tsx b/client-new/src/components/task/Subtask.tsx deleted file mode 100644 index 954a09e..0000000 --- a/client-new/src/components/task/Subtask.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react' -import { View, Text } from 'native-base' -import { moderateScale } from '@/utils/FontSizeUtils' -import { heightPercentageToDP as h, widthPercentageToDP as w } from 'react-native-responsive-screen' -import RightArrowIcon from '@/components/icons/RightArrowIcon' -import { ISubTask } from '@/interfaces/ISubTask' - -type SubTasksProps = { - subtasks: ISubTask -} - -const SubTask = ({ subtasks }: SubTasksProps) => { - return ( - - - - - {subtasks?.sub_task_name} - - - {subtasks?.sub_task_description} - - - - - - - - ) -} - -export default SubTask \ No newline at end of file diff --git a/client-new/src/interfaces/IProgress.ts b/client-new/src/interfaces/IProgress.ts new file mode 100644 index 0000000..c32576c --- /dev/null +++ b/client-new/src/interfaces/IProgress.ts @@ -0,0 +1,13 @@ +import { IModel } from './IModel'; + +export interface ISubtaskProgress extends IModel { + sub_task_id: number; + user_id: number; + completed: boolean; +} + +export interface ITasksProgress extends IModel { + task_id: number; + user_id: number; + progress: number; +} diff --git a/client-new/src/navigation/AppStack.tsx b/client-new/src/navigation/AppStack.tsx index 75f7b68..dbe98c8 100644 --- a/client-new/src/navigation/AppStack.tsx +++ b/client-new/src/navigation/AppStack.tsx @@ -1,8 +1,6 @@ import HomeScreenGuides from '@/components/homescreen components/HomeScreenGuides'; -import FormComponent from '@/components/task/Actions'; import GuideCollectionScreen from '@/screens/app/GuideCollectionScreen'; import GuideScreen from '@/screens/app/GuideScreen'; -import SubTaskScreen from '@/screens/app/tasks/SubTaskScreen'; import SubTaskSummaryScreen from '@/screens/app/tasks/SubTaskSummaryScreen'; import TaskScreen from '@/screens/app/tasks/TaskScreen'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; @@ -10,6 +8,9 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; import React from 'react'; import HomeScreen from './BottomTabNavigator'; +// import TaskStack from './TaskStack'; +import ActionScreen from '@/screens/app/tasks/ActionScreen'; +import GuidesComponent from '@/components/homescreen components/HomeScreenGuides'; const Stack = createNativeStackNavigator(); @@ -24,17 +25,11 @@ export default function AppStack() { - - + + - - + + diff --git a/client-new/src/navigation/BottomTabNavigator.tsx b/client-new/src/navigation/BottomTabNavigator.tsx index b9d1536..9ec8680 100644 --- a/client-new/src/navigation/BottomTabNavigator.tsx +++ b/client-new/src/navigation/BottomTabNavigator.tsx @@ -31,8 +31,8 @@ const TabNavigator = () => { iconComponent = ; } else if (route.name === 'Files') { iconComponent = ; - } else if (route.name === 'Marketplace') { - iconComponent = ; + // } else if (route.name === 'Marketplace') { + // iconComponent = ; } else if (route.name === 'Profile') { iconComponent = ; } @@ -49,7 +49,7 @@ const TabNavigator = () => { > - + {/* */} diff --git a/client-new/src/screens/app/FileCollectionScreen.tsx b/client-new/src/screens/app/FileCollectionScreen.tsx index cde10e1..7765f1b 100644 --- a/client-new/src/screens/app/FileCollectionScreen.tsx +++ b/client-new/src/screens/app/FileCollectionScreen.tsx @@ -98,7 +98,7 @@ export default function FileCollectionScreen() { refetch(); }} colors={['#ff0000', '#00ff00', '#0000ff']} - tintColor={'#ff0000'} + tintColor={'grey'} /> } > diff --git a/client-new/src/screens/app/GuideCollectionScreen.tsx b/client-new/src/screens/app/GuideCollectionScreen.tsx index b8b5268..3711341 100644 --- a/client-new/src/screens/app/GuideCollectionScreen.tsx +++ b/client-new/src/screens/app/GuideCollectionScreen.tsx @@ -14,6 +14,7 @@ import { import { SafeAreaView } from 'react-native-safe-area-context'; import { moderateScale } from '../../utils/FontSizeUtils'; +import BackArrowIcon from '@/components/icons/BackArrow'; export default function GuideCollectionScreen({ navigation }) { const [tagsGuides, setTagsGuides] = useState>( @@ -105,7 +106,12 @@ export default function GuideCollectionScreen({ navigation }) { paddingLeft={w('1.5%')} paddingRight={w('1.5%')} > - + + navigation.goBack()}> + + + + Guides + - + /> {[...tagsGuides.keys()].map((tag, key) => ( - + = ({ children }) => { return ( @@ -75,9 +77,35 @@ const GuideScreen: React.FC = ({ navigation, route }) => { return ( guide && ( - + + + navigation.goBack()}> + + + + Back + + + + + + + - + Guides - navigation.navigate('Guide Screen')}> + navigation.navigate('Guide Collection Screen')}> { + const [formState, setFormState] = useState({}); + const [formErrors, setFormErrors] = useState([]); + const [disabled, setDisabled] = useState(false); + const { user } = useUser(); + const { subtask } = route.params as { subtask: ISubTask }; + + + const { isLoading, error, data: actions } = useQuery({ + queryKey: ['fetchActions', subtask?.id], + queryFn: () => getActions(subtask?.id) + }); + + const handleSubmit = async (e: GestureResponderEvent) => { + e.preventDefault(); + + + try { + setDisabled(true); + setFormState({ ...formState, user_id: user?.id, sub_task_name: subtask.sub_task_name, timestamp: Date.now() }) + await createFile(user?.id, subtask.sub_task_name, formState); + await completeSubTask(user?.id, subtask?.id) + const task = await fetchTask(subtask?.task_id); + navigation.navigate('SubTask Summary Screen', { task: task }); + } + catch (err) { + console.log(err); + } + } + + const renderField = (action, index: number) => { + switch (action.action_type) { + case 'input': + return + case 'select': + return + case 'textarea': + return + case 'checkbox': + return + case 'radio': + return + default: + return null; + } + }; + + if (isLoading) { + return + } + + if (error) { + return ( + + error + + ) + } + + return ( + + + + navigation.goBack()}> + + + + + + + + + {subtask.sub_task_name} + + + {subtask.sub_task_description} + + + {isLoading && } + {error && error } + {actions === null || (actions && Object.keys(actions).length === 0) ? ( + + + + + No Actions Available (yet) + + + ) : ( + + {actions['actions'].map((action: IAction, index: number) => ( + + error.path[0] === action.name)} + key={index} + mt={4} + > + + {action.label} + + {renderField(action, index)} + + + ))} + + + )} + + + ); +} + +export default ActionScreen; + +type SubmitButtonProps = { + handleSubmit: (e: GestureResponderEvent) => void + isDisabled?: boolean +} + +const SubmitButton = ({ handleSubmit, isDisabled }: SubmitButtonProps) => { + return ( + + + + ) +} \ No newline at end of file diff --git a/client-new/src/screens/app/tasks/SubTaskScreen.tsx b/client-new/src/screens/app/tasks/SubTaskScreen.tsx deleted file mode 100644 index 3999b13..0000000 --- a/client-new/src/screens/app/tasks/SubTaskScreen.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { getActions } from '@/services/ActionsService'; -import React, { useEffect } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { Button, ScrollView, Text, View, HStack, Pressable } from 'native-base'; -import Icon from "react-native-vector-icons/Ionicons"; -import LegacyWordmark from '@/components/reusable/LegacyWordmark'; -import FormComponent from '@/components/task/Actions'; -import { ISubTask } from '@/interfaces/ISubTask'; -import BackArrowIcon from '@/components/icons/BackArrow'; -import ActivityLoader from '@/components/reusable/ActivityLoader'; -import { heightPercentageToDP as h, widthPercentageToDP as w } from 'react-native-responsive-screen'; -import { moderateScale, verticalScale } from '@/utils/FontSizeUtils'; -import NoTaskIcon from '@/components/icons/NoTaskIcon'; - -type SubTaskScreenProps = { - route: any - navigation: any -} -const SubTaskScreen = ({ route, navigation }: SubTaskScreenProps) => { - const { subtask } = route.params as { subtask: ISubTask }; - - const { isLoading, error, data } = useQuery({ - queryKey: ['fetchActions1', subtask?.id], - queryFn: () => getActions(subtask?.id) - }); - - useEffect(() => { - console.log(data); - }, [data]); - - return ( - - - - navigation.goBack()}> - - - - - - - - - {subtask.sub_task_name} - - - {subtask.sub_task_description} - - - {isLoading && } - {error && error } - {data === null || (data && Object.keys(data).length === 0) ? ( - - - - - No Actions Available (yet) - - - ) : ( - data && data.actions && ( - - - - ) - )} - - - ); -}; - -export default SubTaskScreen; diff --git a/client-new/src/screens/app/tasks/SubTaskSummaryScreen.tsx b/client-new/src/screens/app/tasks/SubTaskSummaryScreen.tsx index 8b9954e..5899a0b 100644 --- a/client-new/src/screens/app/tasks/SubTaskSummaryScreen.tsx +++ b/client-new/src/screens/app/tasks/SubTaskSummaryScreen.tsx @@ -1,7 +1,7 @@ import { getAllSubTasks } from '@/services/SubTasksService'; import { Button, HStack, Pressable, ScrollView, Text, View } from 'native-base'; import Icon from "react-native-vector-icons/Ionicons"; -import React from "react"; +import React, { useCallback, useEffect } from "react"; import { useQuery } from '@tanstack/react-query'; import LegacyWordmark from '@/components/reusable/LegacyWordmark'; import CircleProgressSubtask from '@/components/reusable/CircleProgressSubtask'; @@ -13,7 +13,8 @@ import { ISubTask } from '@/interfaces/ISubTask'; import { RefreshControl } from 'react-native'; import { heightPercentageToDP as h, widthPercentageToDP as w } from 'react-native-responsive-screen'; import { moderateScale, verticalScale } from '@/utils/FontSizeUtils'; -import SubTask from '@/components/task/Subtask'; +import SubTaskCard from '@/components/task/SubTaskCard'; +import { useIsFocused } from '@react-navigation/native'; // Import useIsFocused hook type SubTaskSummaryScreenProps = { route: any @@ -22,14 +23,23 @@ type SubTaskSummaryScreenProps = { const SubTaskSummaryScreen = ({ route, navigation }: SubTaskSummaryScreenProps) => { const { task } = route.params as { task: ITask }; - - const progress = Math.floor(Math.random() * 100) + 1; + const isFocused = useIsFocused(); // Hook to check if screen is focused const { isLoading, error, data: subtasks, refetch } = useQuery({ queryKey: ['fetchSubTasks', task?.id], queryFn: () => getAllSubTasks(task?.id) }); + const refreshData = useCallback(async () => { + await refetch(); + }, [refetch]); + + useEffect(() => { + if (isFocused) { + refreshData(); + } + }, [isFocused, refreshData]); + return ( {task?.task_description} - + Upcoming Tasks @@ -94,9 +104,7 @@ const SubTaskSummaryScreen = ({ route, navigation }: SubTaskSummaryScreenProps) {error && Error: {error.message}} {subtasks?.length === 0 && No subtasks found} {subtasks?.map((item, index) => ( - navigation.navigate('Subtask Screen', { subtask: item })}> - - + ))} diff --git a/client-new/src/screens/app/tasks/TaskScreen.tsx b/client-new/src/screens/app/tasks/TaskScreen.tsx index 56d8ada..a26a610 100644 --- a/client-new/src/screens/app/tasks/TaskScreen.tsx +++ b/client-new/src/screens/app/tasks/TaskScreen.tsx @@ -11,6 +11,8 @@ import { ActivityIndicator, Pressable, RefreshControl } from 'react-native'; import { ITask } from '@/interfaces/ITask'; import BackArrowIcon from '@/components/icons/BackArrow'; import TaskTagGrid from '@/components/reusable/TaskTagGrid'; +import SearchBar from '@/components/reusable/SearchBar'; +import Fuse from 'fuse.js'; type TaskScreenProps = { navigation: any; @@ -29,6 +31,20 @@ export default function TaskScreen({ navigation }: TaskScreenProps) { staleTime: 60000 // TEMP, unsolved refetch when unncessary }); + const filterTasks = (tasks: ITask[], keys: string[]): ITask[] => { + if (search.length > 0) { + const options = { + keys: keys, + threshold: 0.2 + }; + const fuse = new Fuse(tasks, options); + const fuseResponse = fuse.search(search); + return fuseResponse.map((item) => item.item); + } else { + return tasks; + } + } + return ( <> - setSearch(text)} + justifyContent={'center'} + alignItems={'center'} /> @@ -85,8 +100,8 @@ export default function TaskScreen({ navigation }: TaskScreenProps) { > {isPending && } {error && Error: {error.message}} - {tasks && tasks.length === 0 && No tasks found} - {tasks && tasks.map((item: ITask, index: number) => + {fileteredTasks && fileteredTasks.length === 0 && No tasks found} + {fileteredTasks && fileteredTasks.map((item: ITask, index: number) => navigation.navigate('SubTask Summary Screen', { task: item })} /> diff --git a/client-new/src/services/ActionsService.ts b/client-new/src/services/ActionsService.ts index 3378ab6..4efd2df 100644 --- a/client-new/src/services/ActionsService.ts +++ b/client-new/src/services/ActionsService.ts @@ -1,3 +1,4 @@ +import { IAction } from '@/interfaces/IAction'; import { API_BASE_URL } from '@/services/const'; import { sleep } from '@/utils/MockDelayUtil'; import axios from 'axios'; @@ -7,7 +8,7 @@ export const getActions = async (subtask_id: number) => { const res = await axios.get( `${API_BASE_URL}/subtasks/${subtask_id}/actions` ); - return JSON.parse(res.data); + return JSON.parse(res.data) as IAction[]; } catch (error) { console.log('Error fetching actions', error); throw new Error('Error fetching actions'); diff --git a/client-new/src/services/CreateFileService.ts b/client-new/src/services/CreateFileService.ts deleted file mode 100644 index 61487d8..0000000 --- a/client-new/src/services/CreateFileService.ts +++ /dev/null @@ -1,20 +0,0 @@ -import axios from 'axios'; - -import { API_BASE_URL } from './const'; - -export const createFile = async ( - uid: number, - sub_task_name: string, - data: object -) => { - try { - const res = await axios.post( - `${API_BASE_URL}/files/makepdf/${uid}/${sub_task_name}`, - data - ); - - console.log('Response:', res.data); - } catch (error) { - console.log('Error:', error); - } -}; diff --git a/client-new/src/services/FileService.ts b/client-new/src/services/FileService.ts index 4bb4281..9db66f8 100644 --- a/client-new/src/services/FileService.ts +++ b/client-new/src/services/FileService.ts @@ -77,3 +77,20 @@ export const uploadFile = async (file: DocumentPicker.DocumentPickerAsset, userI throw new Error('Error uploading file'); } } + +export const createFile = async ( + userID: number, + sub_task_name: string, + data: object +) => { + try { + const res = await axios.post( + `${API_BASE_URL}/files/makepdf/${userID}/${sub_task_name}`, + data + ); + + console.log('Response:', res.data); + } catch (error) { + console.log('Error:', error); + } +}; diff --git a/client-new/src/services/SubTasksService.ts b/client-new/src/services/SubTasksService.ts index e4f7d62..db5a300 100644 --- a/client-new/src/services/SubTasksService.ts +++ b/client-new/src/services/SubTasksService.ts @@ -3,6 +3,7 @@ import axios from 'axios'; import { ISubTask } from '../interfaces/ISubTask'; import { API_BASE_URL } from './const'; import { sleep } from '@/utils/MockDelayUtil'; +import { ISubtaskProgress } from '@/interfaces/IProgress'; export const getAllSubTasks = async (taskID: number) => { try { @@ -13,3 +14,24 @@ export const getAllSubTasks = async (taskID: number) => { throw new Error('Error fetching subtasks'); } } + +export const completeSubTask = async (userID: number, subTaskID: number) => { + try { + const response = await axios.put(`${API_BASE_URL}/progresses/subtask/${userID}/${subTaskID}/complete`); + return response.data as ISubTask; + } catch (error) { + console.log('Error completing subtask', error); + throw new Error('Error completing subtask'); + } +} + +export const getSubtaskProgress = async (userID: number, subTaskID: number) => { + console.log(`[SubTasksService] getSubtaskProgress(${userID}, ${subTaskID})`) + try { + const response = await axios.get(`${API_BASE_URL}/progresses/subtask/${userID}/${subTaskID}`); + return response.data as ISubtaskProgress; + } catch (error) { + console.log('Error getting subtask progress', error); + throw new Error('Error getting subtask progress'); + } +} \ No newline at end of file diff --git a/client-new/src/services/TaskService.ts b/client-new/src/services/TaskService.ts index 52f35f2..4c60003 100644 --- a/client-new/src/services/TaskService.ts +++ b/client-new/src/services/TaskService.ts @@ -1,3 +1,4 @@ +import { ITasksProgress } from '@/interfaces/IProgress'; import { ITask } from '@/interfaces/ITask'; import { API_BASE_URL } from '@/services/const'; import { sleep } from '@/utils/MockDelayUtil'; @@ -38,3 +39,24 @@ export const fetchTaskTag = async (taskId: number) => { throw new Error('Error fetching task tag'); } }; + +export const fetchTask = async (taskId: number) => { + try { + const response = await axios.get(`${API_BASE_URL}/tasks/${taskId}`); + return response.data as ITask; + } catch (error) { + console.log('Error fetching task', error); + throw new Error('Error fetching task'); + } +} + +export const getTaskProgress = async (userID: number, taskID: number) => { + console.log(`[TasksService] getTaskProgress(${userID}, ${taskID})`) + try { + const response = await axios.get(`${API_BASE_URL}/progresses/task/${userID}/${taskID}`); + return response.data as ITasksProgress; + } catch (error) { + console.log('Error getting task progress', error); + throw new Error('Error getting task progress'); + } +} \ No newline at end of file diff --git a/server/src/controllers/progress.go b/server/src/controllers/progress.go index 0a3535b..92d18be 100644 --- a/server/src/controllers/progress.go +++ b/server/src/controllers/progress.go @@ -39,18 +39,6 @@ func (p *ProgressController) GetSubTaskProgress(c echo.Context) error { return c.JSON(http.StatusOK, subTaskProgress) } -func (p *ProgressController) CompleteTaskProgress(c echo.Context) error { - userID := c.Param("uid") - taskID := c.Param("tid") - taskProgress, err := p.progressService.CompleteTaskProgress(userID, taskID) - - if err != nil { - return c.JSON(http.StatusNotFound, "failed to complete task progress") - } - - return c.JSON(http.StatusOK, taskProgress) -} - func (p *ProgressController) CompleteSubTaskProgress(c echo.Context) error { userID := c.Param("uid") subTaskID := c.Param("sid") diff --git a/server/src/migrations/data.sql b/server/src/migrations/data.sql index f146c8e..22e40c6 100644 --- a/server/src/migrations/data.sql +++ b/server/src/migrations/data.sql @@ -157,6 +157,127 @@ INSERT INTO sub_tasks (task_id, sub_task_name, sub_task_description, actions) VA "description": "Select your preferred method of payment" } ] +}'), +(2, 'Create Checklist', 'Create a checklist of basic personal information needed for end-of-life planning.', +'{ + "actions": [ + { + "action_type": "textarea", + "label": "List your personal information", + "placeholder": "Enter your personal information", + "name": "personal_information", + "required": true, + "description": "Please provide a list of your personal information" + } + ] +}'), +(6, 'Personal Values', 'Create a comprehensive list of your personal values, preferences, and priorities.', +'{ + "actions": [ + { + "action_type": "textarea", + "label": "List your personal values", + "placeholder": "Enter your personal values", + "name": "personal_values", + "required": true, + "description": "Please provide a list of your personal values" + } + ] +}'), +(6, 'Important Contacts', 'Select 3-5 important contacts to be notified in the event of an emergency.', +'{ + "actions": [ + { + "action_type": "input", + "label": "Contact 1", + "placeholder": "Enter contact 1", + "name": "contact_1", + "type": "text", + "required": true, + "description": "Please provide the name of your first contact" + }, + { + "action_type": "input", + "label": "Contact 2", + "placeholder": "Enter contact 2", + "name": "contact_2", + "type": "text", + "required": true, + "description": "Please provide the name of your second contact" + }, + { + "action_type": "input", + "label": "Contact 3", + "placeholder": "Enter contact 3", + "name": "contact_3", + "type": "text", + "required": true, + "description": "Please provide the name of your third contact" + }, + { + "action_type": "input", + "label": "Contact 4", + "placeholder": "Enter contact 4", + "name": "contact_4", + "type": "text", + "required": false, + "description": "Please provide the name of your fourth contact" + }, + { + "action_type": "input", + "label": "Contact 5", + "placeholder": "Enter contact 5", + "name": "contact_5", + "type": "text", + "required": false, + "description": "Please provide the name of your fifth contact" + } + ] +}'), +(6, 'Organ Donation', 'Document your preferred organ donation choices.', +'{ + "actions": [ + { + "action_type": "radio", + "label": "Organ Donation", + "name": "organ_donation", + "options": [ + "Yes", + "No" + ], + "required": true, + "description": "Please select your preferred organ donation choice" + } + ] +}'), +(6, 'Burial/Cremation', 'Document your preferred burial or cremation preferences.', +'{ + "actions": [ + { + "action_type": "radio", + "label": "Burial or Cremation", + "name": "burial_or_cremation", + "options": [ + "Burial", + "Cremation" + ], + "required": true, + "description": "Please select your preferred burial or cremation choice" + } + ] +}'), +(6, 'Eulogy', 'Create a eulogy for yourself.', +'{ + "actions": [ + { + "action_type": "textarea", + "label": "Eulogy", + "placeholder": "Enter your eulogy", + "name": "eulogy", + "required": true, + "description": "Please provide your eulogy" + } + ] }'); -- Creating test subtasks @@ -164,7 +285,6 @@ INSERT INTO sub_tasks (task_id, sub_task_name, sub_task_description) VALUES (1, 'Research Fear', 'Research books, articles, or podcasts on overcoming fear of death.'), (1, 'Connect With Support', 'Connect with a local support group for individuals facing similar fears.'), (1, 'Manage Anxiety', 'Explore mindfulness or meditation practices to help manage anxiety related to end-of-life topics.'), -(2, 'Create Checklist', 'Create a checklist of basic personal information needed for end-of-life planning.'), (3, 'Your Values', 'Write down your personal values and beliefs.'), (3, 'Causes to Support', 'Identify specific causes or charities you would like to support'), (3, 'Legacy Statement', 'Legacy statement or ethical will to pass on your values to loved ones.'), @@ -173,11 +293,6 @@ INSERT INTO sub_tasks (task_id, sub_task_name, sub_task_description) VALUES (5, 'Financial Inventory', 'Conduct a thorough inventory of all financial accounts and assets.'), (5, 'Life Insurance', 'Review and update your life insurance policies, including coverage amounts and beneficiaries.'), (5, 'Financial Document Storage', 'Store digital copies of important financial documents in a secure location.'), -(6, 'Personal Values', 'Create a comprehensive list of your personal values, preferences'), -(6, 'Important Contacts', 'Create a comprehensive list of important contacts.'), -(6, 'Organ Donation', 'Document your preferred organ donation choices'), -(6, 'Burial/Cremation', 'Document your preferred burial or cremation preferences.'), -(6, 'Eulogy', 'Create a eulogy for yourself'), (7, 'Asset Distribution', 'Leverage draft specific clauses regarding asset distribution, including any conditional bequests.'), (7, 'Minor Children', 'Designate a guardian for minor children and establish trusts for their care.'), (7, 'Digital Assets', 'Include provisions for digital assets, such as passwords and access instructions.'), diff --git a/server/src/models/progress.go b/server/src/models/progress.go index 760088a..629aed8 100644 --- a/server/src/models/progress.go +++ b/server/src/models/progress.go @@ -6,11 +6,11 @@ import ( type TaskProgress struct { types.Model - Completed bool `gorm:"default:false" json:"completed"` - UserID uint `json:"user_id"` - TaskID uint `json:"task_id"` - User *User `gorm:"foreignkey:UserID" json:"-"` - Task *Task `gorm:"foreignkey:TaskID" json:"-"` + Progress uint `gorm:"default:0" json:"progress"` + UserID uint `json:"user_id"` + TaskID uint `json:"task_id"` + User *User `gorm:"foreignkey:UserID" json:"-"` + Task *Task `gorm:"foreignkey:TaskID" json:"-"` } type SubTaskProgress struct { diff --git a/server/src/routes/progress.go b/server/src/routes/progress.go index 62c76ec..86acb4d 100644 --- a/server/src/routes/progress.go +++ b/server/src/routes/progress.go @@ -13,7 +13,7 @@ func ProgressRoutes(g *echo.Group, progressService services.ProgressServiceInter g.GET("/task/:uid/:tid", progressController.GetTaskProgress) g.GET("/subtask/:uid/:sid", progressController.GetSubTaskProgress) g.GET("/:uid/:tid", progressController.GetAllSubTaskProgressOfTask) - g.POST("/task/:uid/:tid", progressController.CompleteTaskProgress) - g.POST("/subtask/:uid/:sid", progressController.CompleteSubTaskProgress) + // g.PUT("/task/:uid/:tid/complete", progressController.CompleteTaskProgress) + g.PUT("/subtask/:uid/:sid/complete", progressController.CompleteSubTaskProgress) } diff --git a/server/src/services/progress.go b/server/src/services/progress.go index 04b4d36..229041a 100644 --- a/server/src/services/progress.go +++ b/server/src/services/progress.go @@ -22,7 +22,6 @@ type ProgressServiceInterface interface { CreateTaskProgress(taskProgress models.TaskProgress) (models.TaskProgress, error) CreateSubTaskProgress(subTaskProgress models.SubTaskProgress) (models.SubTaskProgress, error) - CompleteTaskProgress(uid string, tid string) (models.TaskProgress, error) CompleteSubTaskProgress(uid string, sid string) (models.SubTaskProgress, error) DeleteTaskProgress(id string) error @@ -122,9 +121,9 @@ func (p *ProgressService) CreateAllTaskProgress(id string) ([]models.TaskProgres for _, task := range tasks { taskProgress, err := p.CreateTaskProgress(models.TaskProgress{ - TaskID: task.ID, - UserID: uint(userIDInt), - Completed: false, + TaskID: task.ID, + UserID: uint(userIDInt), + Progress: 0, }) if err != nil { return nil, errors.New("failed to create task progress") @@ -182,33 +181,79 @@ func (p *ProgressService) CreateSubTaskProgress(subTaskProgress models.SubTaskPr return subTaskProgress, nil } -func (p *ProgressService) CompleteTaskProgress(uid string, tid string) (models.TaskProgress, error) { - var existingTaskProgress models.TaskProgress +func (p *ProgressService) CompleteSubTaskProgress(userID string, subTaskID string) (models.SubTaskProgress, error) { + var existingSubTaskProgress models.SubTaskProgress - if err := p.DB.Model(&existingTaskProgress).Where("user_id = ? and task_id = ?", uid, tid).Update("completed", "true").Error; err != nil { - return models.TaskProgress{}, err + if err := p.DB.Model(&existingSubTaskProgress).Where("user_id = ? and sub_task_id = ?", userID, subTaskID).Update("completed", "true").Error; err != nil { + return models.SubTaskProgress{}, err } - if err := p.DB.Where("user_id = ? and task_id = ?", uid, tid).Find(&existingTaskProgress).Error; err != nil { - return models.TaskProgress{}, err + if err := p.DB.Where("user_id = ? and sub_task_id = ?", userID, subTaskID).Find(&existingSubTaskProgress).Error; err != nil { + return models.SubTaskProgress{}, err } - return existingTaskProgress, nil + if err := UpdateTaskProgress(p, userID, subTaskID); err != nil { + return models.SubTaskProgress{}, err + } + return existingSubTaskProgress, nil } -func (p *ProgressService) CompleteSubTaskProgress(uid string, sid string) (models.SubTaskProgress, error) { - var existingSubTaskProgress models.SubTaskProgress +func UpdateTaskProgress(p *ProgressService, userID string, subTaskID string) error { - if err := p.DB.Model(&existingSubTaskProgress).Where("user_id = ? and sub_task_id = ?", uid, sid).Update("completed", "true").Error; err != nil { - return models.SubTaskProgress{}, err + // Get the associated SubTask based on subTaskID + var subTask models.SubTask + if err := p.DB.Where("id = ?", subTaskID).First(&subTask).Error; err != nil { + return err } - if err := p.DB.Where("user_id = ? and sub_task_id = ?", uid, sid).Find(&existingSubTaskProgress).Error; err != nil { - return models.SubTaskProgress{}, err + // Retrieve the TaskID from the SubTask + taskID := subTask.TaskID + + // Get the associated Task using TaskID + var task models.Task + if err := p.DB.Where("id = ?", taskID).First(&task).Error; err != nil { + return err } - return existingSubTaskProgress, nil + var completedCount int + + // Find the total number of subtasks for the associated task + var subTasksProgres []models.SubTaskProgress + subTasksProgres, err := p.GetAllSubTaskProgressOfTask(userID, strconv.Itoa(int(taskID))) + if err != nil { + return err + } + + totalSubTasks := len(subTasksProgres) + + // Count the number of completed subtasks for the associated task + for _, subTaskProgress := range subTasksProgres { + if subTaskProgress.Completed { + completedCount++ + } + } + + // Calculate the progress percentage round to the nearest whole number + var progress uint + if totalSubTasks > 0 { + progress = uint((float64(completedCount) / float64(totalSubTasks)) * 100) + } else { + progress = 0 + } + + // Get the associated TaskProgress using userID and taskID + var taskProgress models.TaskProgress + if err := p.DB.Where("user_id = ? AND task_id = ?", userID, taskID).First(&taskProgress).Error; err != nil { + return err + } + + // Update the TaskProgress with the new progress percentage + if err := p.DB.Model(&taskProgress).Where("user_id = ? AND task_id = ?", userID, taskID).Update("progress", progress).Error; err != nil { + return err + } + + return nil } func (p *ProgressService) DeleteTaskProgress(id string) error {