diff --git a/client/components/DropDownItem.tsx b/client/components/DropDownItem.tsx new file mode 100644 index 0000000..7aa866f --- /dev/null +++ b/client/components/DropDownItem.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { StyleSheet, Text, View } from 'react-native'; + +const DropdownItem = ({ label }: { label: string }) => { + return ( + + {label} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 10 + }, + dropdownLabel: { + fontSize: 18, + color: 'care-wallet-black', + marginRight: 10 + }, + line: { + flex: 1, + height: 1, + backgroundColor: 'gray' + } +}); + +export default DropdownItem; diff --git a/client/components/FilterModal.tsx b/client/components/FilterModal.tsx new file mode 100644 index 0000000..4c6695e --- /dev/null +++ b/client/components/FilterModal.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { + Dimensions, + Modal, + Pressable, + StyleSheet, + Text, + View +} from 'react-native'; + +import DropdownItem from './DropDownItem'; + +const windowHeight = Dimensions.get('window').height; + +const FilterModal = ({ + isVisible, + onClose +}: { + isVisible: boolean; + onClose: () => void; +}) => { + return ( + + + + Filters + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + modalContainer: { + justifyContent: 'center', + alignItems: 'flex-start', // Align items to the left + backgroundColor: 'white', + borderRadius: 20, + padding: 35, + height: windowHeight / 2, + marginTop: windowHeight / 2 + }, + modalText: { + fontSize: 24, + fontWeight: 'bold', + textAlign: 'left', + color: 'black', + marginBottom: 20 + }, + filterOptions: { + padding: 20 + }, + dropdownSeparator: { + borderBottomWidth: 1, + borderBottomColor: 'grey', + marginVertical: 10 + } +}); + +export default FilterModal; diff --git a/client/components/TaskInfoCard.tsx b/client/components/TaskInfoCard.tsx new file mode 100644 index 0000000..b471399 --- /dev/null +++ b/client/components/TaskInfoCard.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { StyleSheet, Text, View } from 'react-native'; + +const TaskInfoComponent = ({ + name, + label, + category, + type, + date +}: { + name: string; + label: string; + category: string; + type: string; + date: Date; +}) => { + const formattedStartDate = date ? new Date(date).toLocaleDateString() : 'N/A'; + + return ( + + + {`Task #${name}`} + {label} + + {`${category} | ${type}`} + {`${formattedStartDate}`} + + ); +}; + +const styles = StyleSheet.create({ + container: { + borderRadius: 10, + borderWidth: 1, + borderColor: '#000000', + padding: 10, + margin: 10, + backgroundColor: '#FFFFFF' + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 10 + }, + taskNumber: { + fontWeight: 'bold', + alignSelf: 'flex-end' + }, + label: { + alignSelf: 'flex-start' + }, + categoryType: { + marginTop: 10 + } +}); + +export default TaskInfoComponent; diff --git a/client/navigation/AppStackBottomTabNavigator.tsx b/client/navigation/AppStackBottomTabNavigator.tsx index 6793920..2421f5a 100644 --- a/client/navigation/AppStackBottomTabNavigator.tsx +++ b/client/navigation/AppStackBottomTabNavigator.tsx @@ -9,6 +9,7 @@ import Home from '../assets/bottom-nav/home.svg'; import User from '../assets/bottom-nav/user.svg'; import MedicationList from '../screens/MedicationList'; import Profile from '../screens/Profile'; +import TaskList from '../screens/TaskList'; const AppStackBottomTab = createBottomTabNavigator(); @@ -36,7 +37,7 @@ export function AppStackBottomTabNavigator() { tabBarIcon: ({ color }) => , tabBarLabel: () => }} - component={MedicationList} + component={TaskList} /> ( + {} + ); + const { tasks, tasksIsLoading } = useFilteredTasks(queryParams); + const [isFilterModalVisible, setIsFilterModalVisible] = useState(false); + + // Fetch task labels for each task (2d array list) + useEffect(() => { + const fetchTaskLabels = async () => { + const labels: { [taskId: string]: string[] } = {}; + if (tasks) { + await Promise.all( + tasks.map(async (task) => { + const labelsForTask = await getTaskLabels(task.task_id.toString()); + labels[task.task_id.toString()] = labelsForTask.map( + (label) => label.label_name + ); + }) + ); + } + setTaskLabels(labels); + }; + + if (tasks) { + fetchTaskLabels(); + } + }, [tasks]); + + // Filter tasks based on search query in multiple fields and labels + const filteredTasks = tasks?.filter((task) => { + const taskFieldsMatch = [ + 'task_id', + 'task_status', + 'task_type', + 'notes' + ].some((field) => + task?.[field] + ?.toString() + .toLowerCase() + .includes(searchQuery.toLowerCase()) + ); + + const labelMatch = taskLabels[task?.task_id?.toString()]?.some((label) => + label.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return taskFieldsMatch || labelMatch; + }); + + // Filter tasks based on categories + const pastDueTasks = tasks?.filter( + (task) => task?.end_date || '' < String(new Date()) + ); + const inProgressTasks = tasks?.filter( + (task) => task?.task_status === 'PARTIAL' + ); + const inFutureTasks = tasks?.filter( + (task) => task?.start_date || '' > String(new Date()) + ); + const completeTasks = tasks?.filter( + (task) => task?.task_status === 'COMPLETE' + ); + const incompleteTasks = tasks?.filter( + (task) => task?.task_status === 'INCOMPLETE' + ); + + // Abstraction to render each section + const renderSection = (tasks: Task[], title: string) => { + return ( + + {title} + {tasks.map((task, index) => { + return ( + + ); + })} + + ); + }; + + return ( + + + { + setSearchQuery(text); + }} + /> + setIsFilterModalVisible(true)} + > + Filter + + + + Task List (all tasks of all time) + + {renderSection(filteredTasks || [], 'All Tasks')} + {renderSection(pastDueTasks || [], 'Past Due')} + {renderSection(inProgressTasks || [], 'In Progress')} + {renderSection(inFutureTasks || [], 'Future')} + {renderSection(completeTasks || [], 'Done')} + {renderSection(incompleteTasks || [], 'Marked as Incomplete')} + setIsFilterModalVisible(false)} + /> + + ); +} + +// TODO: Migrate this to tailwind +const styles = StyleSheet.create({ + container: { + padding: 20 + }, + searchContainer: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 10 + }, + searchInput: { + flex: 1, + height: 40, + borderColor: 'gray', + borderWidth: 1, + borderRadius: 20, + marginRight: 10, + padding: 8, + overflow: 'hidden' + }, + filterButton: { + backgroundColor: 'gray', + borderRadius: 5, + padding: 8 + }, + filterButtonText: { + color: 'white' + } +}); \ No newline at end of file diff --git a/client/services/task.ts b/client/services/task.ts new file mode 100644 index 0000000..cc5e167 --- /dev/null +++ b/client/services/task.ts @@ -0,0 +1,43 @@ +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import axios from 'axios'; + +import { Task } from '../types/task'; +import { TaskLabel } from '../types/label'; +import { api_url } from './api-links'; + +type TaskQueryParams = { + taskID?: string; + groupID?: string; + createdBy?: string; + taskStatus?: string; + taskType?: string; + startDate?: string; + endDate?: string; +}; + +const getFilteredTasks = async (queryParams: TaskQueryParams): Promise => { + const { data } = await axios.get(`${api_url}/tasks/filtered?`, { + params: queryParams, + }); + return data; +}; + +export const getTaskLabels = async (taskID: string): Promise => { + const { data } = await axios.get(`${api_url}/tasks/${taskID}/labels`); + return data; +}; + +export const useFilteredTasks = (queryParams: TaskQueryParams) => { + const queryClient = useQueryClient(); + + const { data: tasks, isLoading: tasksIsLoading } = useQuery({ + queryKey: ['filteredTaskList', queryParams], + queryFn: () => getFilteredTasks(queryParams), + refetchInterval: 20000, + }); + + return { + tasks, + tasksIsLoading, + }; +}; \ No newline at end of file diff --git a/client/types/label.ts b/client/types/label.ts new file mode 100644 index 0000000..c91852b --- /dev/null +++ b/client/types/label.ts @@ -0,0 +1,5 @@ +export interface TaskLabel { + task_id: number; + group_id: number; + label_name: string; +} \ No newline at end of file diff --git a/client/types/task.ts b/client/types/task.ts new file mode 100644 index 0000000..104f5d7 --- /dev/null +++ b/client/types/task.ts @@ -0,0 +1,16 @@ +export interface Task { + task_id: number; + group_id: number; + created_by: string; + created_date: string; + start_date?: string | null; + end_date?: string | null; + notes?: string | null; + repeating: boolean; + repeating_interval?: string | null; + repeating_end_date?: string | null; + task_status: string; + task_type: string; + task_info?: string | null; + [key: string]: string | number | boolean | null | undefined; // Index signature for string indexing +}; \ No newline at end of file