From f08270ab3f6e2c920f8f87092768372cc97accec Mon Sep 17 00:00:00 2001 From: David Oduneye Date: Thu, 7 Dec 2023 23:46:19 -0500 Subject: [PATCH 1/3] style: formatting --- .../HomeScreenTaskCard.tsx | 4 +- .../src/components/icons/RightArrowIcon.tsx | 10 +- .../components/reusable/CircleProgress.tsx | 55 +++-- .../reusable/CircleProgressSubtask.tsx | 46 ++-- client-new/src/components/task/Actions.tsx | 125 ----------- client-new/src/components/task/InputField.tsx | 2 +- .../src/components/task/SubTaskCard.tsx | 88 ++++++++ client-new/src/components/task/Subtask.tsx | 60 ----- client-new/src/interfaces/IProgress.ts | 13 ++ client-new/src/navigation/AppStack.tsx | 6 +- .../src/navigation/BottomTabNavigator.tsx | 6 +- .../src/screens/app/FileCollectionScreen.tsx | 2 +- .../src/screens/app/tasks/ActionScreen.tsx | 205 ++++++++++++++++++ .../src/screens/app/tasks/SubTaskScreen.tsx | 93 -------- .../app/tasks/SubTaskSummaryScreen.tsx | 8 +- client-new/src/services/ActionsService.ts | 3 +- client-new/src/services/CreateFileService.ts | 20 -- client-new/src/services/FileService.ts | 17 ++ client-new/src/services/SubTasksService.ts | 22 ++ client-new/src/services/TaskService.ts | 22 ++ client-new/src/services/const.ts | 2 +- server/src/controllers/progress.go | 18 +- server/src/models/progress.go | 52 ++++- server/src/routes/progress.go | 4 +- server/src/services/progress.go | 29 ++- 25 files changed, 532 insertions(+), 380 deletions(-) delete mode 100644 client-new/src/components/task/Actions.tsx create mode 100644 client-new/src/components/task/SubTaskCard.tsx delete mode 100644 client-new/src/components/task/Subtask.tsx create mode 100644 client-new/src/interfaces/IProgress.ts create mode 100644 client-new/src/screens/app/tasks/ActionScreen.tsx delete mode 100644 client-new/src/screens/app/tasks/SubTaskScreen.tsx delete mode 100644 client-new/src/services/CreateFileService.ts diff --git a/client-new/src/components/homescreen components/HomeScreenTaskCard.tsx b/client-new/src/components/homescreen components/HomeScreenTaskCard.tsx index 27a6124..324cf6b 100644 --- a/client-new/src/components/homescreen components/HomeScreenTaskCard.tsx +++ b/client-new/src/components/homescreen components/HomeScreenTaskCard.tsx @@ -37,8 +37,6 @@ const HomeScreenTaskCard: React.FC = ({ task, isAllTasks, handleOnPre } }, [isAllTasks, task.id]); - const progress = Math.floor(Math.random() * 100) + 1; - return ( = ({ task, isAllTasks, handleOnPre {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..61d3c03 100644 --- a/client-new/src/components/reusable/CircleProgress.tsx +++ b/client-new/src/components/reusable/CircleProgress.tsx @@ -1,31 +1,52 @@ -import React, { useEffect, useRef } from 'react'; +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, Text, View } from 'react-native'; import Svg, { Circle } from 'react-native-svg'; 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 strokeWidth = 10; + const { isLoading, error, data: progress, refetch } = useQuery({ + queryKey: ['fetchTaskProgress', task?.id], + queryFn: () => getTaskProgress(user?.id, task?.id) + }); + + 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; + const [progressStrokeDashoffset, setProgressStrokeDashoffset] = useState(0); useEffect(() => { - Animated.timing(animatedValue, { - toValue: progress, + + const offset = ((progress.progress / 100) * circumference) / 100; + setProgressStrokeDashoffset(offset); + + Animated?.timing(animatedValue, { + toValue: progress?.progress ? progress?.progress : 0, duration: 1000, useNativeDriver: true }).start(); - }, [animatedValue, progress]); + }, [animatedValue, progress?.progress]); const { left, top } = - progress < 10 - ? { left: 26, top: 27 } - : progress >= 100 - ? { left: 17, top: 27 } - : { left: 20, top: 27 }; + progress?.progress ? progress?.progress : 0 < 10 + ? { left: 165, top: 41 } + : progress?.progress ? progress?.progress : 0 < 100 + ? { left: 156, top: 41 } + : { left: 159, top: 41 }; + + console.log('[home screen] Progress:', progress); + return ( @@ -48,7 +69,7 @@ const CircleProgress = ({ progress }) => { strokeDasharray={circumference} strokeDashoffset={animatedValue.interpolate({ inputRange: [0, 100], - outputRange: [circumference, progressStrokeDashoffset] + outputRange: [circumference, progressStrokeDashoffset ? progressStrokeDashoffset : 0] })} strokeLinecap="round" fill="none" @@ -58,13 +79,13 @@ const CircleProgress = ({ progress }) => { - {`${progress}%`} + {progress?.progress}% ); diff --git a/client-new/src/components/reusable/CircleProgressSubtask.tsx b/client-new/src/components/reusable/CircleProgressSubtask.tsx index ab53976..0a08394 100644 --- a/client-new/src/components/reusable/CircleProgressSubtask.tsx +++ b/client-new/src/components/reusable/CircleProgressSubtask.tsx @@ -1,31 +1,51 @@ -import React, { useEffect, useRef } from 'react'; +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, Text, View } from 'react-native'; import Svg, { Circle } from 'react-native-svg'; 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 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; + const [progressStrokeDashoffset, setProgressStrokeDashoffset] = useState(0); useEffect(() => { - Animated.timing(animatedValue, { - toValue: progress, + + const offset = ((progress.progress / 100) * circumference) / 100; + setProgressStrokeDashoffset(offset); + + Animated?.timing(animatedValue, { + toValue: progress?.progress, duration: 1000, useNativeDriver: true }).start(); - }, [animatedValue, progress]); + }, [animatedValue, progress?.progress]); const { left, top } = - progress < 10 + progress?.progress < 10 ? { left: 165, top: 41 } - : progress >= 100 + : progress?.progress < 100 ? { left: 156, top: 41 } : { left: 159, top: 41 }; + + console.log('Progress:', progress); return ( @@ -48,7 +68,7 @@ const CircleProgress = ({ progress }) => { strokeDasharray={circumference} strokeDashoffset={animatedValue.interpolate({ inputRange: [0, 100], - outputRange: [circumference, progressStrokeDashoffset] + outputRange: [circumference, progressStrokeDashoffset ? progressStrokeDashoffset : 0] })} strokeLinecap="round" fill="transparent" @@ -64,12 +84,12 @@ const CircleProgress = ({ progress }) => { fontSize: 20 }} > - {`${progress}%`} + {isLoading ? '0%' : ''} + {error ? '0%' : ''} + {progress ? `${progress?.progress}%` : ''} ); }; 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..d8ac3ea 100644 --- a/client-new/src/components/task/InputField.tsx +++ b/client-new/src/components/task/InputField.tsx @@ -24,7 +24,7 @@ const InputField: React.FC = (InputFieldProps) => { 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..18f4c8d --- /dev/null +++ b/client-new/src/components/task/SubTaskCard.tsx @@ -0,0 +1,88 @@ +import React 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' + +type SubTasksProps = { + subtask: ISubTask + navigation: any +} + +const SubTaskCard = ({ subtask, navigation }: SubTasksProps) => { + const { user } = useUser(); + + const { isLoading, error, data: complete } = useQuery({ + queryKey: ['fetchSubtaskProgress', user?.id, subtask?.id], + queryFn: () => getSubtaskProgress(user?.id, subtask?.id) + }); + + if (isLoading) { + return Loading... + }; + + if (error) { + return Error + }; + + console.log('Subtask Progress:', complete) + + return ( + navigation.navigate('Subtask 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 17b13d6..0e6f6a1 100644 --- a/client-new/src/navigation/AppStack.tsx +++ b/client-new/src/navigation/AppStack.tsx @@ -7,8 +7,8 @@ import React from 'react'; import HomeScreen from './BottomTabNavigator'; // import TaskStack from './TaskStack'; import SubTaskSummaryScreen from '@/screens/app/tasks/SubTaskSummaryScreen'; -import SubTaskScreen from '@/screens/app/tasks/SubTaskScreen'; -import FormComponent from '@/components/task/Actions'; +import FormComponent from '@/screens/app/tasks/ActionScreen'; +import ActionScreen from '@/screens/app/tasks/ActionScreen'; const Stack = createNativeStackNavigator(); @@ -24,7 +24,7 @@ export default function AppStack() { - + diff --git a/client-new/src/navigation/BottomTabNavigator.tsx b/client-new/src/navigation/BottomTabNavigator.tsx index 94d2620..5f8b0af 100644 --- a/client-new/src/navigation/BottomTabNavigator.tsx +++ b/client-new/src/navigation/BottomTabNavigator.tsx @@ -29,8 +29,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 = ; } @@ -47,7 +47,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/tasks/ActionScreen.tsx b/client-new/src/screens/app/tasks/ActionScreen.tsx new file mode 100644 index 0000000..2405f49 --- /dev/null +++ b/client-new/src/screens/app/tasks/ActionScreen.tsx @@ -0,0 +1,205 @@ +import { useUser } from '@/contexts/UserContext'; +import { IAction } from '@/interfaces/IAction'; +import { createFile } from '@/services/FileService'; +import { + Button, + Checkbox, + FormControl, + HStack, + ScrollView, + Select, + Text, + View, + Pressable +} from 'native-base'; +import { heightPercentageToDP as h, widthPercentageToDP as w } 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 '../../../components/task/CheckboxField'; +import RadioField from '../../../components/task/RadioField'; +import { GestureResponderEvent } from 'react-native'; +import { ISubTask } from '@/interfaces/ISubTask'; +import { useQuery } from '@tanstack/react-query'; +import { getActions } from '@/services/ActionsService'; +import BackArrowIcon from '@/components/icons/BackArrow'; +import NoTaskIcon from '@/components/icons/NoTaskIcon'; +import ActivityLoader from '@/components/reusable/ActivityLoader'; +import LegacyWordmark from '@/components/reusable/LegacyWordmark'; +import { moderateScale, verticalScale } from '@/utils/FontSizeUtils'; +import { fetchTask } from '@/services/TaskService'; +import { completeSubTask } from '@/services/SubTasksService'; + + +type ActionScreenProps = { + // actions: IAction[]; + // subTaskName: string; + navigation: any; + route: any +}; + +const ActionScreen = ({ navigation, route }: ActionScreenProps) => { + const [formState, setFormState] = useState({}); + const [formErrors, setFormErrors] = useState([]); + 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 { + 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 +} + +const SubmitButton = ({ handleSubmit }: 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..2a46d37 100644 --- a/client-new/src/screens/app/tasks/SubTaskSummaryScreen.tsx +++ b/client-new/src/screens/app/tasks/SubTaskSummaryScreen.tsx @@ -13,7 +13,7 @@ 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'; type SubTaskSummaryScreenProps = { route: any @@ -79,7 +79,7 @@ const SubTaskSummaryScreen = ({ route, navigation }: SubTaskSummaryScreenProps) textAlign={"center"}> {task?.task_description} - + Upcoming Tasks @@ -94,9 +94,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/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/client-new/src/services/const.ts b/client-new/src/services/const.ts index 7282564..2906329 100644 --- a/client-new/src/services/const.ts +++ b/client-new/src/services/const.ts @@ -1,6 +1,6 @@ /** * API_BASE_URL is a constant that is used to determine the base URL for the API. */ -export const API_BASE_URL = false +export const API_BASE_URL = true ? 'http://localhost:8080/api' : 'https://legacy.loca.lt/api'; diff --git a/server/src/controllers/progress.go b/server/src/controllers/progress.go index 0a3535b..66c8fc4 100644 --- a/server/src/controllers/progress.go +++ b/server/src/controllers/progress.go @@ -39,17 +39,17 @@ 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) +// 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") - } +// if err != nil { +// return c.JSON(http.StatusNotFound, "failed to complete task progress") +// } - return c.JSON(http.StatusOK, taskProgress) -} +// return c.JSON(http.StatusOK, taskProgress) +// } func (p *ProgressController) CompleteSubTaskProgress(c echo.Context) error { userID := c.Param("uid") diff --git a/server/src/models/progress.go b/server/src/models/progress.go index 760088a..4d9ac11 100644 --- a/server/src/models/progress.go +++ b/server/src/models/progress.go @@ -2,15 +2,17 @@ package models import ( "server/src/types" + + "gorm.io/gorm" ) 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 { @@ -21,3 +23,43 @@ type SubTaskProgress struct { User *User `gorm:"foreignkey:UserID" json:"-"` SubTask *SubTask `gorm:"foreignkey:SubTaskID" json:"-"` } + +func UpdateTaskProgress(db *gorm.DB, subTaskID uint) error { + var completedCount int64 + + // Count the number of completed subtasks for the associated task + if err := db.Model(&SubTaskProgress{}).Where("sub_task_id = ? AND completed = ?", subTaskID, true).Count(&completedCount).Error; err != nil { + return err + } + + // Find the total number of subtasks for the associated task + var totalSubTasks int64 + if err := db.Model(&SubTask{}).Where("task_id = ?", subTaskID).Count(&totalSubTasks).Error; err != nil { + return err + } + + // Calculate the progress percentage round to the nearest whole number + progress := uint((float64(completedCount) / float64(totalSubTasks)) * 100) + + // Update the TaskProgress based on the calculated progress + var subTask SubTask + if err := db.First(&subTask, subTaskID).Error; err != nil { + return err + } + + if err := db.Model(&TaskProgress{}).Where("task_id = ?", subTask.TaskID).Updates(TaskProgress{Progress: progress}).Error; err != nil { + return err + } + + return nil +} + +func (s *SubTaskProgress) BeforeSave(tx *gorm.DB) (err error) { + if s.Completed { // Check if the subtask is being marked as completed + // Update the task progress + if err = UpdateTaskProgress(tx, s.SubTaskID); err != nil { + return err + } + } + return nil +} 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..9164594 100644 --- a/server/src/services/progress.go +++ b/server/src/services/progress.go @@ -22,7 +22,7 @@ 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) + // CompleteTaskProgress(uid string, tid string) (models.TaskProgress, error) CompleteSubTaskProgress(uid string, sid string) (models.SubTaskProgress, error) DeleteTaskProgress(id string) error @@ -122,9 +122,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,20 +182,19 @@ 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 - - if err := p.DB.Model(&existingTaskProgress).Where("user_id = ? and task_id = ?", uid, tid).Update("completed", "true").Error; err != nil { - return models.TaskProgress{}, err - } +// func (p *ProgressService) CompleteTaskProgress(uid string, tid string) (models.TaskProgress, error) { +// var existingTaskProgress models.TaskProgress - 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.Model(&existingTaskProgress).Where("user_id = ? and task_id = ?", uid, tid).Update("completed", "true").Error; err != nil { +// return models.TaskProgress{}, err +// } - return existingTaskProgress, nil +// if err := p.DB.Where("user_id = ? and task_id = ?", uid, tid).Find(&existingTaskProgress).Error; err != nil { +// return models.TaskProgress{}, err +// } -} +// return existingTaskProgress, nil +// } func (p *ProgressService) CompleteSubTaskProgress(uid string, sid string) (models.SubTaskProgress, error) { var existingSubTaskProgress models.SubTaskProgress From d2b7ca041c5e07117058a2952325c8e15fb3a811 Mon Sep 17 00:00:00 2001 From: David Oduneye Date: Fri, 8 Dec 2023 04:15:30 -0500 Subject: [PATCH 2/3] style: formatting --- client-new/App.tsx | 5 ++ .../components/reusable/CircleProgress.tsx | 55 ++++++------- .../reusable/CircleProgressSubtask.tsx | 76 +++++++++-------- .../src/components/task/SubTaskCard.tsx | 22 ++++- client-new/src/navigation/AppStack.tsx | 8 +- .../src/navigation/BottomTabNavigator.tsx | 2 +- .../src/screens/app/GuideCollectionScreen.tsx | 3 +- .../src/screens/app/tasks/ActionScreen.tsx | 14 ++-- .../app/tasks/SubTaskSummaryScreen.tsx | 16 +++- .../src/screens/app/tasks/TaskScreen.tsx | 41 +++++++--- server/src/controllers/progress.go | 12 --- server/src/models/progress.go | 42 ---------- server/src/services/progress.go | 82 +++++++++++++++---- 13 files changed, 207 insertions(+), 171 deletions(-) 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/reusable/CircleProgress.tsx b/client-new/src/components/reusable/CircleProgress.tsx index 61d3c03..65d243e 100644 --- a/client-new/src/components/reusable/CircleProgress.tsx +++ b/client-new/src/components/reusable/CircleProgress.tsx @@ -3,8 +3,9 @@ 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, Text, View } from 'react-native'; +import { Animated } from 'react-native'; import Svg, { Circle } from 'react-native-svg'; +import { Text, View } from 'native-base'; const AnimatedCircle = Animated.createAnimatedComponent(Circle); @@ -24,29 +25,26 @@ const CircleProgress = ({ task }: CircleProgressProps) => { const strokeWidth = 13; const radius = 50 - strokeWidth / 2; const circumference = 2 * Math.PI * radius; - const [progressStrokeDashoffset, setProgressStrokeDashoffset] = useState(0); useEffect(() => { - - const offset = ((progress.progress / 100) * circumference) / 100; - setProgressStrokeDashoffset(offset); - - Animated?.timing(animatedValue, { - toValue: progress?.progress ? progress?.progress : 0, - duration: 1000, - useNativeDriver: true - }).start(); + if (progress?.progress !== undefined) { + Animated.timing(animatedValue, { + toValue: progress.progress || 0, + duration: 1000, + useNativeDriver: true + }).start(); + } }, [animatedValue, progress?.progress]); - const { left, top } = - progress?.progress ? progress?.progress : 0 < 10 - ? { left: 165, top: 41 } - : progress?.progress ? progress?.progress : 0 < 100 - ? { left: 156, top: 41 } - : { left: 159, top: 41 }; - - console.log('[home screen] Progress:', progress); + const progressStrokeDashoffset = animatedValue.interpolate({ + inputRange: [0, 100], + outputRange: [circumference, 0] + }); + const textPosition = { + left: 43 - (progress?.progress < 10 ? 6 : 10), + top: 42 - (progress?.progress < 10 ? 6 : 10) + }; return ( @@ -66,26 +64,19 @@ const CircleProgress = ({ task }: CircleProgressProps) => { r={radius} stroke="#43A573" strokeWidth={strokeWidth} - strokeDasharray={circumference} - strokeDashoffset={animatedValue.interpolate({ - inputRange: [0, 100], - outputRange: [circumference, progressStrokeDashoffset ? progressStrokeDashoffset : 0] - })} + strokeDasharray={`${circumference} ${circumference}`} + strokeDashoffset={progressStrokeDashoffset} strokeLinecap="round" fill="none" transform="rotate(-90 50 50)" /> - {progress?.progress}% + {progress?.progress || 0}% ); diff --git a/client-new/src/components/reusable/CircleProgressSubtask.tsx b/client-new/src/components/reusable/CircleProgressSubtask.tsx index 0a08394..75e5ed9 100644 --- a/client-new/src/components/reusable/CircleProgressSubtask.tsx +++ b/client-new/src/components/reusable/CircleProgressSubtask.tsx @@ -2,9 +2,11 @@ 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, Text, View } from 'react-native'; +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); @@ -14,38 +16,51 @@ type CircleProgressProps = { 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, setProgressStrokeDashoffset] = useState(0); useEffect(() => { - - const offset = ((progress.progress / 100) * circumference) / 100; - setProgressStrokeDashoffset(offset); - - Animated?.timing(animatedValue, { - toValue: progress?.progress, - duration: 1000, - useNativeDriver: true - }).start(); + if (progress?.progress !== undefined) { + Animated.timing(animatedValue, { + toValue: progress.progress || 0, + duration: 1000, + useNativeDriver: true + }).start(); + } }, [animatedValue, progress?.progress]); - const { left, top } = - progress?.progress < 10 - ? { left: 165, top: 41 } - : progress?.progress < 100 - ? { left: 156, top: 41 } - : { left: 159, top: 41 }; + 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) + }; - console.log('Progress:', progress); return ( @@ -57,7 +72,7 @@ const CircleProgress = ({ task }: CircleProgressProps) => { 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 ? progressStrokeDashoffset : 0] - })} + strokeDashoffset={progressStrokeDashoffset} strokeLinecap="round" fill="transparent" transform="rotate(-90 50 50)" /> - {isLoading ? '0%' : ''} - {error ? '0%' : ''} - {progress ? `${progress?.progress}%` : ''} + {progress?.progress || 0}% ); diff --git a/client-new/src/components/task/SubTaskCard.tsx b/client-new/src/components/task/SubTaskCard.tsx index 18f4c8d..f3a396c 100644 --- a/client-new/src/components/task/SubTaskCard.tsx +++ b/client-new/src/components/task/SubTaskCard.tsx @@ -1,4 +1,4 @@ -import React from 'react' +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' @@ -8,6 +8,7 @@ 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 @@ -16,8 +17,9 @@ type SubTasksProps = { const SubTaskCard = ({ subtask, navigation }: SubTasksProps) => { const { user } = useUser(); + const isFocused = useIsFocused(); // Hook to check if screen is focused - const { isLoading, error, data: complete } = useQuery({ + const { isLoading, error, data: complete, refetch } = useQuery({ queryKey: ['fetchSubtaskProgress', user?.id, subtask?.id], queryFn: () => getSubtaskProgress(user?.id, subtask?.id) }); @@ -30,11 +32,25 @@ const SubTaskCard = ({ subtask, navigation }: SubTasksProps) => { return Error }; + const refreshData = useCallback(async () => { + // Refetch subtasks data here + console.log('[SubTaskSummaryScreen] Refreshing data...') + console.log(complete) + await refetch(); + }, [refetch]); + + + useEffect(() => { + if (isFocused) { + console.log('[SubTaskCard] Refreshing data...') + refreshData(); // Refresh data when screen gains focus + } + }, [isFocused, refetch]); console.log('Subtask Progress:', complete) return ( navigation.navigate('Subtask Screen', { subtask: subtask })} + onPress={() => navigation.navigate('Action Screen', { subtask: subtask })} isDisabled={complete?.completed} > - + - + diff --git a/client-new/src/navigation/BottomTabNavigator.tsx b/client-new/src/navigation/BottomTabNavigator.tsx index b6fa929..9ec8680 100644 --- a/client-new/src/navigation/BottomTabNavigator.tsx +++ b/client-new/src/navigation/BottomTabNavigator.tsx @@ -48,7 +48,7 @@ const TabNavigator = () => { })} > - + {/* */} diff --git a/client-new/src/screens/app/GuideCollectionScreen.tsx b/client-new/src/screens/app/GuideCollectionScreen.tsx index b8b5268..6b35a3a 100644 --- a/client-new/src/screens/app/GuideCollectionScreen.tsx +++ b/client-new/src/screens/app/GuideCollectionScreen.tsx @@ -131,7 +131,7 @@ export default function GuideCollectionScreen({ navigation }) { {[...tagsGuides.keys()].map((tag, key) => ( - + { const [formState, setFormState] = useState({}); const [formErrors, setFormErrors] = useState([]); + const [disabled, setDisabled] = useState(false); const { user } = useUser(); const { subtask } = route.params as { subtask: ISubTask }; @@ -58,11 +56,11 @@ const ActionScreen = ({ navigation, route }: ActionScreenProps) => { 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) { @@ -166,7 +164,7 @@ const ActionScreen = ({ navigation, route }: ActionScreenProps) => { ))} - + )} @@ -178,9 +176,10 @@ export default ActionScreen; type SubmitButtonProps = { handleSubmit: (e: GestureResponderEvent) => void + isDisabled?: boolean } -const SubmitButton = ({ handleSubmit }: SubmitButtonProps) => { +const SubmitButton = ({ handleSubmit, isDisabled }: SubmitButtonProps) => { return ( { borderColor={'#43A573'} onPress={handleSubmit} flex={0.9} + isDisabled={isDisabled ? isDisabled : false} > Submit diff --git a/client-new/src/screens/app/tasks/SubTaskSummaryScreen.tsx b/client-new/src/screens/app/tasks/SubTaskSummaryScreen.tsx index 2a46d37..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'; @@ -14,6 +14,7 @@ import { RefreshControl } from 'react-native'; import { heightPercentageToDP as h, widthPercentageToDP as w } from 'react-native-responsive-screen'; import { moderateScale, verticalScale } from '@/utils/FontSizeUtils'; 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 ( { + 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/server/src/controllers/progress.go b/server/src/controllers/progress.go index 66c8fc4..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/models/progress.go b/server/src/models/progress.go index 4d9ac11..629aed8 100644 --- a/server/src/models/progress.go +++ b/server/src/models/progress.go @@ -2,8 +2,6 @@ package models import ( "server/src/types" - - "gorm.io/gorm" ) type TaskProgress struct { @@ -23,43 +21,3 @@ type SubTaskProgress struct { User *User `gorm:"foreignkey:UserID" json:"-"` SubTask *SubTask `gorm:"foreignkey:SubTaskID" json:"-"` } - -func UpdateTaskProgress(db *gorm.DB, subTaskID uint) error { - var completedCount int64 - - // Count the number of completed subtasks for the associated task - if err := db.Model(&SubTaskProgress{}).Where("sub_task_id = ? AND completed = ?", subTaskID, true).Count(&completedCount).Error; err != nil { - return err - } - - // Find the total number of subtasks for the associated task - var totalSubTasks int64 - if err := db.Model(&SubTask{}).Where("task_id = ?", subTaskID).Count(&totalSubTasks).Error; err != nil { - return err - } - - // Calculate the progress percentage round to the nearest whole number - progress := uint((float64(completedCount) / float64(totalSubTasks)) * 100) - - // Update the TaskProgress based on the calculated progress - var subTask SubTask - if err := db.First(&subTask, subTaskID).Error; err != nil { - return err - } - - if err := db.Model(&TaskProgress{}).Where("task_id = ?", subTask.TaskID).Updates(TaskProgress{Progress: progress}).Error; err != nil { - return err - } - - return nil -} - -func (s *SubTaskProgress) BeforeSave(tx *gorm.DB) (err error) { - if s.Completed { // Check if the subtask is being marked as completed - // Update the task progress - if err = UpdateTaskProgress(tx, s.SubTaskID); err != nil { - return err - } - } - return nil -} diff --git a/server/src/services/progress.go b/server/src/services/progress.go index 9164594..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 @@ -182,34 +181,81 @@ 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 - -// 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.Where("user_id = ? and task_id = ?", uid, tid).Find(&existingTaskProgress).Error; err != nil { -// return models.TaskProgress{}, err -// } - -// return existingTaskProgress, nil -// } - -func (p *ProgressService) CompleteSubTaskProgress(uid string, sid string) (models.SubTaskProgress, error) { +func (p *ProgressService) CompleteSubTaskProgress(userID string, subTaskID string) (models.SubTaskProgress, error) { var existingSubTaskProgress models.SubTaskProgress - if err := p.DB.Model(&existingSubTaskProgress).Where("user_id = ? and sub_task_id = ?", uid, sid).Update("completed", "true").Error; err != nil { + 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 sub_task_id = ?", uid, sid).Find(&existingSubTaskProgress).Error; err != nil { + if err := p.DB.Where("user_id = ? and sub_task_id = ?", userID, subTaskID).Find(&existingSubTaskProgress).Error; err != nil { + return models.SubTaskProgress{}, err + } + + if err := UpdateTaskProgress(p, userID, subTaskID); err != nil { return models.SubTaskProgress{}, err } return existingSubTaskProgress, nil } +func UpdateTaskProgress(p *ProgressService, userID string, subTaskID string) error { + + // 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 + } + + // 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 + } + + 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 { var taskProgress models.TaskProgress From 3ae60d7b6a1b01453e688b79078c1f62fb54cce9 Mon Sep 17 00:00:00 2001 From: David Oduneye Date: Fri, 8 Dec 2023 05:33:28 -0500 Subject: [PATCH 3/3] finish? --- .../HomeScreenTaskCard.tsx | 51 ++++++- .../components/reusable/CircleProgress.tsx | 1 + client-new/src/components/task/InputField.tsx | 6 +- .../src/components/task/SubTaskCard.tsx | 25 ++-- client-new/src/navigation/AppStack.tsx | 2 +- .../src/screens/app/GuideCollectionScreen.tsx | 15 ++- client-new/src/screens/app/GuideScreen.tsx | 34 ++++- client-new/src/screens/app/HomeScreen.tsx | 2 +- .../src/screens/app/tasks/ActionScreen.tsx | 2 +- server/src/migrations/data.sql | 127 +++++++++++++++++- 10 files changed, 225 insertions(+), 40 deletions(-) diff --git a/client-new/src/components/homescreen components/HomeScreenTaskCard.tsx b/client-new/src/components/homescreen components/HomeScreenTaskCard.tsx index 324cf6b..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,8 +64,19 @@ const HomeScreenTaskCard: React.FC = ({ task, isAllTasks, handleOnPre } }, [isAllTasks, task.id]); + 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} diff --git a/client-new/src/components/reusable/CircleProgress.tsx b/client-new/src/components/reusable/CircleProgress.tsx index 65d243e..f6148dd 100644 --- a/client-new/src/components/reusable/CircleProgress.tsx +++ b/client-new/src/components/reusable/CircleProgress.tsx @@ -75,6 +75,7 @@ const CircleProgress = ({ task }: CircleProgressProps) => { position={'absolute'} left={`${textPosition.left}%`} top={`${textPosition.top}%`} + color={progress?.progress === 100 ? '#00000033' : '#2F1D12'} > {progress?.progress || 0}% diff --git a/client-new/src/components/task/InputField.tsx b/client-new/src/components/task/InputField.tsx index d8ac3ea..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,12 +15,12 @@ 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 { diff --git a/client-new/src/components/task/SubTaskCard.tsx b/client-new/src/components/task/SubTaskCard.tsx index f3a396c..58ad4ca 100644 --- a/client-new/src/components/task/SubTaskCard.tsx +++ b/client-new/src/components/task/SubTaskCard.tsx @@ -17,36 +17,31 @@ type SubTasksProps = { const SubTaskCard = ({ subtask, navigation }: SubTasksProps) => { const { user } = useUser(); - const isFocused = useIsFocused(); // Hook to check if screen is focused + const isFocused = useIsFocused(); const { isLoading, error, data: complete, refetch } = useQuery({ queryKey: ['fetchSubtaskProgress', user?.id, subtask?.id], queryFn: () => getSubtaskProgress(user?.id, subtask?.id) }); - if (isLoading) { - return Loading... - }; - - if (error) { - return Error - }; - const refreshData = useCallback(async () => { - // Refetch subtasks data here - console.log('[SubTaskSummaryScreen] Refreshing data...') - console.log(complete) await refetch(); }, [refetch]); useEffect(() => { if (isFocused) { - console.log('[SubTaskCard] Refreshing data...') - refreshData(); // Refresh data when screen gains focus + refreshData(); } }, [isFocused, refetch]); - console.log('Subtask Progress:', complete) + + if (isLoading) { + return Loading... + }; + + if (error) { + return Error + }; return ( - + diff --git a/client-new/src/screens/app/GuideCollectionScreen.tsx b/client-new/src/screens/app/GuideCollectionScreen.tsx index 6b35a3a..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) => ( diff --git a/client-new/src/screens/app/GuideScreen.tsx b/client-new/src/screens/app/GuideScreen.tsx index 1509c4a..2186e48 100644 --- a/client-new/src/screens/app/GuideScreen.tsx +++ b/client-new/src/screens/app/GuideScreen.tsx @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'; import { Box, Image, ScrollView, Text, View } from 'native-base'; import React from 'react'; -import { ActivityIndicator } from 'react-native'; +import { ActivityIndicator, Pressable } from 'react-native'; import { heightPercentageToDP as hp, widthPercentageToDP as wp @@ -12,6 +12,8 @@ import { import { fetchGuideByName } from '../../services/GuideService'; import { getMonth } from '../../utils/DateUtils'; import { moderateScale, verticalScale } from '../../utils/FontSizeUtils'; +import BackArrowIcon from '@/components/icons/BackArrow'; +import LegacyWordmark from '@/components/reusable/LegacyWordmark'; const MarkdownWrapper: React.FC = ({ 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')}> { return ( - + navigation.goBack()}> 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.'),