From 298a2cbf72c04952516bdf7763073e2d0b6f0270 Mon Sep 17 00:00:00 2001 From: Chaiyawut Thengket Date: Thu, 23 Nov 2023 01:20:48 +0700 Subject: [PATCH 01/45] write api dashboard for reccurence task --- backend/dashboard/serializers.py | 12 +-- backend/dashboard/urls.py | 5 +- backend/dashboard/views.py | 156 ++++++++++++++++++++++++++++++- 3 files changed, 163 insertions(+), 10 deletions(-) diff --git a/backend/dashboard/serializers.py b/backend/dashboard/serializers.py index ddc207b..8563ec4 100644 --- a/backend/dashboard/serializers.py +++ b/backend/dashboard/serializers.py @@ -1,7 +1,7 @@ -from rest_framework import serializers -from .models import UserStats +# from rest_framework import serializers +# from .models import UserStats -class UserStatsSerializer(serializers.ModelSerializer): - class Meta: - model = UserStats - fields = ['health', 'gold', 'experience', 'strength', 'intelligence', 'endurance', 'perception', 'luck', 'level'] \ No newline at end of file +# class UserStatsSerializer(serializers.ModelSerializer): +# class Meta: +# model = UserStats +# fields = ['health', 'gold', 'experience', 'strength', 'intelligence', 'endurance', 'perception', 'luck', 'level'] \ No newline at end of file diff --git a/backend/dashboard/urls.py b/backend/dashboard/urls.py index 56624ca..a383dd6 100644 --- a/backend/dashboard/urls.py +++ b/backend/dashboard/urls.py @@ -1,11 +1,12 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .views import DashboardStatsViewSet, DashboardWeeklyViewSet +from .views import DashboardStatsTodoViewSet, DashboardWeeklyViewSet router = DefaultRouter() -router.register(r'dashboard/stats', DashboardStatsViewSet, basename='stats') +router.register(r'dashboard/stats', DashboardStatsTodoViewSet, basename='statstodo') router.register(r'dashboard/weekly', DashboardWeeklyViewSet, basename='weekly') +router.register(r'dashboard/stats', DashboardStatsTodoViewSet, basename='statsrec') urlpatterns = [ path('', include(router.urls)), ] diff --git a/backend/dashboard/views.py b/backend/dashboard/views.py index abe0576..0f191ab 100644 --- a/backend/dashboard/views.py +++ b/backend/dashboard/views.py @@ -5,10 +5,13 @@ from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from rest_framework import viewsets, mixins -from tasks.models import Todo +from tasks.models import Todo, RecurrenceTask -class DashboardStatsViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): +class DashboardStatsTodoViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): + """ + A viewset for retrieving statistics related to user tasks for the last 7 days. + """ permission_classes = (IsAuthenticated,) def get_queryset(self): @@ -66,6 +69,17 @@ class DashboardStatsViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): # Overall completion rate total_tasks = Todo.objects.filter(user=user).count() overall_completion_rate = (completed_last_7_days / total_tasks) * 100 if total_tasks > 0 else 0 + + total_completed_tasks = Todo.objects.filter(user=user, completed=True).count() + + total_tasks = Todo.objects.filter(user=user).count() + + tasks_completed_today = Todo.objects.filter( + user=user, + completed=True, + completion_date__gte=timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + ).count() + data = { "completed_last_7_days": completed_last_7_days, @@ -75,6 +89,9 @@ class DashboardStatsViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): "completed_this_week": completed_this_week, "overdue_tasks": overdue_tasks, "overall_completion_rate": overall_completion_rate, + "total_completed_tasks": total_completed_tasks, + "total_tasks" : total_tasks, + "tasks_completed_today": tasks_completed_today, } return Response(data, status=status.HTTP_200_OK) @@ -145,7 +162,142 @@ class DashboardWeeklyViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): return Response(weekly_stats, status=status.HTTP_200_OK) +class DashboardStatsReccurenceViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): + """ + A viewset for retrieving statistics related to user tasks for the last 7 days. + """ + permission_classes = (IsAuthenticated,) + def get_queryset(self): + return RecurrenceTask.objects.all() + + def list(self, request, *args, **kwargs): + user = self.request.user + + # Calculate the start and end date for the last 7 days + end_date = timezone.now() + start_date = end_date - timedelta(days=7) + + # How many tasks were completed in the last 7 days + completed_last_7_days = RecurrenceTask.objects.filter( + user=user, + completed=True, + completion_date__gte=start_date, + completion_date__lte=end_date + ).count() + + # Task assign last week compared with this week + tasks_assigned_last_week = RecurrenceTask.objects.filter( + user=user, + completion_date__gte=start_date - timedelta(days=7), + completion_date__lte=start_date + ).count() + + tasks_assigned_this_week = RecurrenceTask.objects.filter( + user=user, + completion_date__gte=start_date, + completion_date__lte=end_date + ).count() + + # Completed tasks from last week compared with this week + completed_last_week = RecurrenceTask.objects.filter( + user=user, + completed=True, + completion_date__gte=start_date - timedelta(days=7), + completion_date__lte=start_date + ).count() + + completed_this_week = RecurrenceTask.objects.filter( + user=user, + completed=True, + completion_date__gte=start_date, + completion_date__lte=end_date + ).count() + + overdue_tasks = RecurrenceTask.objects.filter( + user=user, + completed=False, + end_event__lt=timezone.now() + ).count() + + # Overall completion rate + total_tasks = RecurrenceTask.objects.filter(user=user).count() + overall_completion_rate = (completed_last_7_days / total_tasks) * 100 if total_tasks > 0 else 0 + + total_completed_tasks = RecurrenceTask.objects.filter( + user=user, + completed=True + ).count() + + total_tasks = RecurrenceTask.objects.filter(user=user).count() + + tasks_completed_today = RecurrenceTask.objects.filter( + user=user, + completed=True, + completion_date__gte=timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + ).count() + + data = { + "completed_last_7_days": completed_last_7_days, + "tasks_assigned_last_week": tasks_assigned_last_week, + "tasks_assigned_this_week": tasks_assigned_this_week, + "completed_last_week": completed_last_week, + "completed_this_week": completed_this_week, + "overdue_tasks": overdue_tasks, + "overall_completion_rate": overall_completion_rate, + "total_completed_tasks": total_completed_tasks, + "total_tasks" : total_tasks, + "tasks_completed_today": tasks_completed_today, + } + + return Response(data, status=status.HTTP_200_OK) + +# class DashboardStatsAllViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): +# permission_classes = [IsAuthenticated] + +# def get_queryset(self): +# return Todo.objects.filter(user=self.request.user) + +# def list(self, request, *args, **kwargs): +# user = request.user + +# # Calculate task usage statistics +# todo_count = self.get_queryset().count() +# recurrence_task_count = RecurrenceTask.objects.filter(user=user).count() + +# # Calculate how many tasks were completed in the last 7 days +# completed_todo_count_last_week = Todo.objects.filter(user=user, completed=True, last_update__gte=timezone.now() - timezone.timedelta(days=7)).count() +# completed_recurrence_task_count_last_week = RecurrenceTask.objects.filter(user=user, completed=True, last_update__gte=timezone.now() - timezone.timedelta(days=7)).count() + +# # Calculate subtask completion rate +# total_subtasks = Todo.objects.filter(user=user).aggregate(total=Count('subtask__id'))['total'] +# completed_subtasks = Todo.objects.filter(user=user, subtask__completed=True).aggregate(total=Count('subtask__id'))['total'] + +# # Calculate overall completion rate +# total_tasks = todo_count + recurrence_task_count +# completed_tasks = completed_todo_count_last_week + completed_recurrence_task_count_last_week +# overall_completion_rate = (completed_tasks / total_tasks) * 100 if total_tasks > 0 else 0 + +# # pie chart show +# complete_todo_percent_last_week = (completed_todo_count_last_week / todo_count) * 100 if todo_count > 0 else 0 +# complete_recurrence_percent_last_week = (completed_recurrence_task_count_last_week / recurrence_task_count) * 100 if recurrence_task_count > 0 else 0 +# incomplete_task_percent_last_week = 100 - complete_recurrence_percent_last_week - complete_todo_percent_last_week + +# data = { +# 'todo_count': todo_count, +# 'recurrence_task_count': recurrence_task_count, +# 'completed_todo_count_last_week': completed_todo_count_last_week, +# 'completed_recurrence_task_count_last_week': completed_recurrence_task_count_last_week, +# 'total_subtasks': total_subtasks, +# 'completed_subtasks': completed_subtasks, +# 'overall_completion_rate': overall_completion_rate, +# 'complete_todo_percent_last_week': complete_todo_percent_last_week, +# 'complete_recurrence_percent_last_week' : complete_recurrence_percent_last_week, +# 'incomplete_task_percent_last_week': incomplete_task_percent_last_week, +# } + +# return Response(data, status=status.HTTP_200_OK) + # class DashboardStatsAPIView(APIView): # permission_classes = [IsAuthenticated] From f6e9489a14e9b43b2d1fe02d93b39858af7ef6b9 Mon Sep 17 00:00:00 2001 From: Chaiyawut Thengket Date: Sat, 25 Nov 2023 13:17:13 +0700 Subject: [PATCH 02/45] add new url --- backend/dashboard/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/dashboard/urls.py b/backend/dashboard/urls.py index a383dd6..af8d154 100644 --- a/backend/dashboard/urls.py +++ b/backend/dashboard/urls.py @@ -4,9 +4,9 @@ from rest_framework.routers import DefaultRouter from .views import DashboardStatsTodoViewSet, DashboardWeeklyViewSet router = DefaultRouter() -router.register(r'dashboard/stats', DashboardStatsTodoViewSet, basename='statstodo') +router.register(r'dashboard/todostats', DashboardStatsTodoViewSet, basename='statstodo') router.register(r'dashboard/weekly', DashboardWeeklyViewSet, basename='weekly') -router.register(r'dashboard/stats', DashboardStatsTodoViewSet, basename='statsrec') +router.register(r'dashboard/recstats', DashboardStatsTodoViewSet, basename='statsrec') urlpatterns = [ path('', include(router.urls)), ] From ea6353a4555a1f4a90966aeffcc11eb90c221c06 Mon Sep 17 00:00:00 2001 From: THIS ONE IS A LITTLE BIT TRICKY KRUB Date: Sat, 25 Nov 2023 14:34:23 +0700 Subject: [PATCH 03/45] Refactor dashboard component imports and remove unused code --- .../src/components/dashboard/PieChart.jsx | 43 +++++++++++++++ .../src/components/dashboard/dashboard.jsx | 53 +++++++++++-------- 2 files changed, 73 insertions(+), 23 deletions(-) create mode 100644 frontend/src/components/dashboard/PieChart.jsx diff --git a/frontend/src/components/dashboard/PieChart.jsx b/frontend/src/components/dashboard/PieChart.jsx new file mode 100644 index 0000000..73f132b --- /dev/null +++ b/frontend/src/components/dashboard/PieChart.jsx @@ -0,0 +1,43 @@ +import { DonutChart } from "@tremor/react"; +import axiosInstance from "../../api/configs/AxiosConfig"; + +const fetchPieData = async () => { + try { + let res = await axiosInstance.get("/dashboard/stats/"); + // let todoCount = res.data.todo_count; + // let recurrenceCount = res.data.recurrence_count; + let todoCount = 10; + let recurrenceCount = 15; + if (todoCount === undefined) { + todoCount = 0; + } + if (recurrenceCount === undefined) { + recurrenceCount = 0; + } + const donutData = [ + { name: "Completed Tasks", count: todoCount }, + { name: "Uncompleted Tasks", count: recurrenceCount }, + ]; + return donutData; + } catch (error) { + console.error("Error fetching donut data:", error); + return []; + } +}; + +const pieDataArray = await fetchPieData(); + +export function PieChartGraph() { + return ( + + ); +} diff --git a/frontend/src/components/dashboard/dashboard.jsx b/frontend/src/components/dashboard/dashboard.jsx index 31cc71f..7825141 100644 --- a/frontend/src/components/dashboard/dashboard.jsx +++ b/frontend/src/components/dashboard/dashboard.jsx @@ -1,22 +1,29 @@ -import { Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title, Legend } from "@tremor/react"; -import { KpiCard } from "./KpiCard"; +import { + Card, + Grid, + Tab, + TabGroup, + TabList, + TabPanel, + TabPanels, + Text, + Title, + Legend, +} from "@tremor/react"; +import {KpiCard} from "./KpiCard"; import { BarChartGraph } from "./Barchart"; -import { DonutChartGraph } from "./DonutChart"; import { AreaChartGraph } from "./Areachart"; -import { ProgressCircleChart } from "./ProgressCircle"; -import { useState } from "react"; +import {ProgressCircleChart} from "./ProgressCircle"; + +const valueFormatter = (number) => + `$ ${new Intl.NumberFormat("us").format(number).toString()}`; export function Dashboard() { - const [value, setValue] = useState({ - from: new Date(2021, 0, 1), - to: new Date(2023, 0, 7), - }); return (
Dashboard All of your progress will be shown right here. -
@@ -40,7 +47,8 @@ export function Dashboard() { + colors={["indigo"]} + > @@ -51,18 +59,17 @@ export function Dashboard() { -
- - Tasks - -
- -
-
+
+ + Tasks +
+ +
+
From debcbef7caaec1c2649bf30f2cbb43894e8cdf1a Mon Sep 17 00:00:00 2001 From: THIS ONE IS A LITTLE BIT TRICKY KRUB Date: Sat, 25 Nov 2023 14:48:29 +0700 Subject: [PATCH 04/45] Refactor dashboard components and update API endpoints --- .../src/components/dashboard/DonutChart.jsx | 39 -------------- frontend/src/components/dashboard/KpiCard.jsx | 2 +- .../src/components/dashboard/PieChart.jsx | 51 +++++++++---------- .../components/dashboard/ProgressCircle.jsx | 2 +- .../src/components/dashboard/dashboard.jsx | 12 +++-- 5 files changed, 34 insertions(+), 72 deletions(-) delete mode 100644 frontend/src/components/dashboard/DonutChart.jsx diff --git a/frontend/src/components/dashboard/DonutChart.jsx b/frontend/src/components/dashboard/DonutChart.jsx deleted file mode 100644 index 63fc591..0000000 --- a/frontend/src/components/dashboard/DonutChart.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import { DonutChart } from "@tremor/react"; -import { axiosInstance } from "src/api/AxiosConfig"; -import { useState, useEffect } from "react"; - -export function DonutChartGraph() { - const [donutData, setDonutData] = useState([]); - - useEffect(() => { - const fetchDonutData = async () => { - try { - const response = await axiosInstance.get("/dashboard/stats/"); - const todoCount = response.data.todo_count || 0; - const recurrenceCount = response.data.recurrence_count || 0; - - const donutData = [ - { name: "Todo", count: todoCount }, - { name: "Recurrence", count: recurrenceCount }, - ]; - - setDonutData(donutData); - } catch (error) { - console.error("Error fetching donut data:", error); - } - }; - fetchDonutData(); - }, []); - - return ( - - ); -} diff --git a/frontend/src/components/dashboard/KpiCard.jsx b/frontend/src/components/dashboard/KpiCard.jsx index c5c3165..85c74c9 100644 --- a/frontend/src/components/dashboard/KpiCard.jsx +++ b/frontend/src/components/dashboard/KpiCard.jsx @@ -13,7 +13,7 @@ export function KpiCard() { useEffect(() => { const fetchKpiCardData = async () => { try { - const response = await axiosInstance.get("/dashboard/stats/"); + const response = await axiosInstance.get("/dashboard/todostats/"); const completedThisWeek = response.data.completed_this_week || 0; const completedLastWeek = response.data.completed_last_week || 0; const percentage = (completedThisWeek / completedLastWeek) * 100; diff --git a/frontend/src/components/dashboard/PieChart.jsx b/frontend/src/components/dashboard/PieChart.jsx index 73f132b..645137b 100644 --- a/frontend/src/components/dashboard/PieChart.jsx +++ b/frontend/src/components/dashboard/PieChart.jsx @@ -1,37 +1,34 @@ import { DonutChart } from "@tremor/react"; -import axiosInstance from "../../api/configs/AxiosConfig"; +import { axiosInstance } from "src/api/AxiosConfig"; +import { useState, useEffect } from "react"; -const fetchPieData = async () => { - try { - let res = await axiosInstance.get("/dashboard/stats/"); - // let todoCount = res.data.todo_count; - // let recurrenceCount = res.data.recurrence_count; - let todoCount = 10; - let recurrenceCount = 15; - if (todoCount === undefined) { - todoCount = 0; - } - if (recurrenceCount === undefined) { - recurrenceCount = 0; - } - const donutData = [ - { name: "Completed Tasks", count: todoCount }, - { name: "Uncompleted Tasks", count: recurrenceCount }, - ]; - return donutData; - } catch (error) { - console.error("Error fetching donut data:", error); - return []; - } -}; +export function DonutChartGraph() { + const [donutData, setDonutData] = useState([]); -const pieDataArray = await fetchPieData(); + useEffect(() => { + const fetchDonutData = async () => { + try { + const response = await axiosInstance.get("/dashboard/todostats/"); + const totalTask = response.data.total_tasks || 0; + const completedTask = response.data.total_completed_tasks || 0; + + const donutData = [ + { name: "Completed task", count: totalTask }, + { name: "Total task", count: completedTask }, + ]; + + setDonutData(donutData); + } catch (error) { + console.error("Error fetching donut data:", error); + } + }; + fetchDonutData(); + }, []); -export function PieChartGraph() { return ( { const fetchProgressData = async () => { try { - const response = await axiosInstance.get("/dashboard/stats/"); + const response = await axiosInstance.get("/dashboard/todostats/"); let completedLastWeek = response.data.completed_last_week || 0; let assignLastWeek = response.data.tasks_assigned_last_week || 0; diff --git a/frontend/src/components/dashboard/dashboard.jsx b/frontend/src/components/dashboard/dashboard.jsx index 7825141..47bcaba 100644 --- a/frontend/src/components/dashboard/dashboard.jsx +++ b/frontend/src/components/dashboard/dashboard.jsx @@ -10,10 +10,11 @@ import { Title, Legend, } from "@tremor/react"; -import {KpiCard} from "./KpiCard"; +import { KpiCard } from "./KpiCard"; import { BarChartGraph } from "./Barchart"; import { AreaChartGraph } from "./Areachart"; -import {ProgressCircleChart} from "./ProgressCircle"; +import { DonutChartGraph } from "./PieChart"; +import { ProgressCircleChart } from "./ProgressCircle"; const valueFormatter = (number) => `$ ${new Intl.NumberFormat("us").format(number).toString()}`; @@ -59,17 +60,20 @@ export function Dashboard() { -
+ +
Tasks +
+
From 778594ab9a83db1dea3816c5d73412a54f49ea6b Mon Sep 17 00:00:00 2001 From: Chaiyawut Thengket Date: Sat, 25 Nov 2023 15:17:09 +0700 Subject: [PATCH 05/45] add total_tasks_today --- backend/dashboard/views.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/backend/dashboard/views.py b/backend/dashboard/views.py index 0f191ab..b6fd552 100644 --- a/backend/dashboard/views.py +++ b/backend/dashboard/views.py @@ -74,10 +74,20 @@ class DashboardStatsTodoViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): total_tasks = Todo.objects.filter(user=user).count() + today_start = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + today_end = timezone.now().replace(hour=23, minute=59, second=59, microsecond=999999) + tasks_completed_today = Todo.objects.filter( user=user, completed=True, - completion_date__gte=timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + completion_date__gte=today_start, + completion_date__lte=today_end + ).count() + + total_tasks_today = Todo.objects.filter( + user=user, + completion_date__gte=today_start, + completion_date__lte=today_end ).count() @@ -91,6 +101,7 @@ class DashboardStatsTodoViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): "overall_completion_rate": overall_completion_rate, "total_completed_tasks": total_completed_tasks, "total_tasks" : total_tasks, + "total_tasks_today": total_tasks_today, "tasks_completed_today": tasks_completed_today, } From c29543407f6de41378cb6081a79c3575c5c53cf7 Mon Sep 17 00:00:00 2001 From: THIS ONE IS A LITTLE BIT TRICKY KRUB Date: Sat, 25 Nov 2023 15:18:21 +0700 Subject: [PATCH 06/45] Fix capitalization of Dashboard component and add task statistics to Overview tab --- frontend/src/App.jsx | 2 +- .../src/components/dashboard/dashboard.jsx | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a917ca5..625b9b8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -11,7 +11,7 @@ import { SideNav } from "./components/navigations/IconSideNav"; import { Eisenhower } from "./components/EisenhowerMatrix/Eisenhower"; import { PrivateRoute } from "./PrivateRoute"; import { ProfileUpdatePage } from "./components/profile/profilePage"; -import { Dashboard } from "./components/dashboard/dashboard"; +import { Dashboard } from "./components/dashboard/Dashboard"; import { LandingPage } from "./components/landingPage/LandingPage"; import { PublicRoute } from "./PublicRoute"; import { useAuth } from "./hooks/AuthHooks"; diff --git a/frontend/src/components/dashboard/dashboard.jsx b/frontend/src/components/dashboard/dashboard.jsx index 47bcaba..9e77163 100644 --- a/frontend/src/components/dashboard/dashboard.jsx +++ b/frontend/src/components/dashboard/dashboard.jsx @@ -9,17 +9,43 @@ import { Text, Title, Legend, + Metric, } from "@tremor/react"; import { KpiCard } from "./KpiCard"; import { BarChartGraph } from "./Barchart"; import { AreaChartGraph } from "./Areachart"; import { DonutChartGraph } from "./PieChart"; import { ProgressCircleChart } from "./ProgressCircle"; +import { axiosInstance } from "src/api/AxiosConfig"; +import { useEffect, useState } from "react"; const valueFormatter = (number) => `$ ${new Intl.NumberFormat("us").format(number).toString()}`; export function Dashboard() { + const [totalTask, setTotalTask] = useState(0); + const [totalCompletedTasks, settotalCompletedTasks] = useState(0); + const [totalCompletedTasksToday, setTotalCompletedTasksToday] = useState(0); + const [totalTaskToday, setTotalTaskToday] = useState(0); + + useEffect(() => { + const fetchData = async () => { + const response = await axiosInstance.get("/dashboard/todostats/"); + const totalTaskValue = response.data.total_tasks || 0; + const totalCompletedTasksValue = response.data.total_completed_tasks || 0; + const totalCompletedTasksTodayValue = + response.data.total_completed_tasks_today || 0; + const totalTaskToday = response.data.total_task_today || 0; + + setTotalTask(totalTaskValue); + settotalCompletedTasks(totalCompletedTasksValue); + setTotalCompletedTasksToday(totalCompletedTasksTodayValue); + setTotalTaskToday(totalTaskToday); + }; + + fetchData(); + }, []); + return (
@@ -59,8 +85,38 @@ export function Dashboard() { + {/*Overview Tab*/} + + + Total tasks + {totalTask} + +

+ + Total completed tasks + {totalCompletedTasks} + +

+ + Task completed today + {totalCompletedTasksToday} + +
+ {/*Pie chart graph*/}
Tasks From 779e45aa897159e909fb992a01f0f65fcaf3f6f5 Mon Sep 17 00:00:00 2001 From: THIS ONE IS A LITTLE BIT TRICKY KRUB Date: Sat, 25 Nov 2023 15:41:54 +0700 Subject: [PATCH 07/45] completed dashboard. --- .../src/components/dashboard/dashboard.jsx | 66 ++++++++++++++----- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/dashboard/dashboard.jsx b/frontend/src/components/dashboard/dashboard.jsx index 9e77163..1901ebb 100644 --- a/frontend/src/components/dashboard/dashboard.jsx +++ b/frontend/src/components/dashboard/dashboard.jsx @@ -10,6 +10,8 @@ import { Title, Legend, Metric, + ProgressCircle, + Flex, } from "@tremor/react"; import { KpiCard } from "./KpiCard"; import { BarChartGraph } from "./Barchart"; @@ -26,7 +28,8 @@ export function Dashboard() { const [totalTask, setTotalTask] = useState(0); const [totalCompletedTasks, settotalCompletedTasks] = useState(0); const [totalCompletedTasksToday, setTotalCompletedTasksToday] = useState(0); - const [totalTaskToday, setTotalTaskToday] = useState(0); + const [progressData, setProgressData] = useState(0); + const [overdueTask, setOverdueTask] = useState(0); useEffect(() => { const fetchData = async () => { @@ -36,11 +39,18 @@ export function Dashboard() { const totalCompletedTasksTodayValue = response.data.total_completed_tasks_today || 0; const totalTaskToday = response.data.total_task_today || 0; + const totalCompletedTasksToday = response.data.tasks_completed_today || 0; + const overdueTasks = response.data.overdue_tasks || 0; + + const progress = + (totalCompletedTasksToday / totalCompletedTasksToday) * 100; setTotalTask(totalTaskValue); settotalCompletedTasks(totalCompletedTasksValue); setTotalCompletedTasksToday(totalCompletedTasksTodayValue); setTotalTaskToday(totalTaskToday); + setProgressData(progress); + setOverdueTask(overdueTasks); }; fetchData(); @@ -73,7 +83,7 @@ export function Dashboard() { @@ -89,6 +99,7 @@ export function Dashboard() { + Overview - Task completed today - {totalCompletedTasksToday} + Overdue tasks + {overdueTask} +

{/*Pie chart graph*/} -
- - Tasks - -
+ + Overall completion rate + +
+ +
+ {/*Progress circle graph*/} + + + Today's progress +
+ + + + {progressData.toFixed(0)} % + + +

-
-
+ categories={["Completed Tasks"]} + colors={["rose"]} + > + +
From 75f194386fae20c52ba38fdf525416ead57e7ec8 Mon Sep 17 00:00:00 2001 From: Pattadon Date: Sun, 26 Nov 2023 01:13:32 +0700 Subject: [PATCH 08/45] Refactor code to handle NaN and infinite values in KpiCard and ProgressCircleChart components --- frontend/src/components/dashboard/KpiCard.jsx | 19 ++++++++-- .../components/dashboard/ProgressCircle.jsx | 13 ++++++- .../src/components/dashboard/dashboard.jsx | 4 +- .../components/kanbanBoard/kanbanBoard.jsx | 38 +++++++++++++++---- .../src/components/kanbanBoard/taskCard.jsx | 11 ++++-- .../kanbanBoard/taskDetailModal.jsx | 3 ++ 6 files changed, 72 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/dashboard/KpiCard.jsx b/frontend/src/components/dashboard/KpiCard.jsx index 85c74c9..b1d02b3 100644 --- a/frontend/src/components/dashboard/KpiCard.jsx +++ b/frontend/src/components/dashboard/KpiCard.jsx @@ -1,4 +1,11 @@ -import { BadgeDelta, Card, Flex, Metric, ProgressBar, Text } from "@tremor/react"; +import { + BadgeDelta, + Card, + Flex, + Metric, + ProgressBar, + Text, +} from "@tremor/react"; import { useEffect, useState } from "react"; import { axiosInstance } from "src/api/AxiosConfig"; @@ -46,10 +53,16 @@ export function KpiCard() {
{kpiCardData.completedThisWeek}
- {kpiCardData.percentage.toFixed(0)}% + + {isNaN(kpiCardData.percentage) || !isFinite(kpiCardData.percentage) + ? "0%" + : `${kpiCardData.percentage.toFixed(0)}%`} + - vs. {kpiCardData.completedLastWeek} (last week) + + vs. {kpiCardData.completedLastWeek} (last week) + diff --git a/frontend/src/components/dashboard/ProgressCircle.jsx b/frontend/src/components/dashboard/ProgressCircle.jsx index 4a290d9..aa3a0a3 100644 --- a/frontend/src/components/dashboard/ProgressCircle.jsx +++ b/frontend/src/components/dashboard/ProgressCircle.jsx @@ -33,9 +33,18 @@ export function ProgressCircleChart() { return ( - + - {progressData.toFixed(0)} % + {isNaN(progressData) || !isFinite(progressData) + ? "0%" + : `${progressData.toFixed(0)}%`} diff --git a/frontend/src/components/dashboard/dashboard.jsx b/frontend/src/components/dashboard/dashboard.jsx index 1901ebb..005f98a 100644 --- a/frontend/src/components/dashboard/dashboard.jsx +++ b/frontend/src/components/dashboard/dashboard.jsx @@ -154,7 +154,9 @@ export function Dashboard() { color="rose" > - {progressData.toFixed(0)} % + {isNaN(progressData) || !isFinite(progressData) + ? "0%" + : `${progressData.toFixed(0)}%`}

diff --git a/frontend/src/components/kanbanBoard/kanbanBoard.jsx b/frontend/src/components/kanbanBoard/kanbanBoard.jsx index 2bda08b..2635cff 100644 --- a/frontend/src/components/kanbanBoard/kanbanBoard.jsx +++ b/frontend/src/components/kanbanBoard/kanbanBoard.jsx @@ -1,6 +1,12 @@ import { useMemo, useState, useEffect } from "react"; import { ColumnContainerCard } from "./columnContainerWrapper"; -import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; +import { + DndContext, + DragOverlay, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; import { SortableContext, arrayMove } from "@dnd-kit/sortable"; import { createPortal } from "react-dom"; import { TaskCard } from "./taskCard"; @@ -26,7 +32,9 @@ export function KanbanBoard() { // ---------------- Task Handlers ---------------- const handleTaskUpdate = (tasks, updatedTask) => { - const updatedTasks = tasks.map((task) => (task.id === updatedTask.id ? updatedTask : task)); + const updatedTasks = tasks.map((task) => + task.id === updatedTask.id ? updatedTask : task + ); setTasks(updatedTasks); }; @@ -168,8 +176,14 @@ export function KanbanBoard() { justify-center overflow-x-auto overflow-y-hidden - "> - + " + > +
{!isLoading ? ( @@ -181,7 +195,9 @@ export function KanbanBoard() { createTask={createTask} deleteTask={deleteTask} updateTask={updateTask} - tasks={(tasks || []).filter((task) => task.columnId === col.id)} + tasks={(tasks || []).filter( + (task) => task.columnId === col.id + )} /> ))}{" "} @@ -194,7 +210,11 @@ export function KanbanBoard() { {createPortal( {/* Render the active task as a draggable overlay */} - + , document.body )} @@ -302,7 +322,11 @@ export function KanbanBoard() { const isOverAColumn = over.data.current?.type === "Column"; // Move the Task to a different column and update columnId - if (isActiveATask && isOverAColumn && tasks.some((task) => task.columnId !== overId)) { + if ( + isActiveATask && + isOverAColumn && + tasks.some((task) => task.columnId !== overId) + ) { setTasks((tasks) => { const activeIndex = tasks.findIndex((t) => t.id === activeId); axiosInstance diff --git a/frontend/src/components/kanbanBoard/taskCard.jsx b/frontend/src/components/kanbanBoard/taskCard.jsx index 1a151c8..e631a26 100644 --- a/frontend/src/components/kanbanBoard/taskCard.jsx +++ b/frontend/src/components/kanbanBoard/taskCard.jsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { useEffect } from "react"; import { BsFillTrashFill } from "react-icons/bs"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; @@ -6,6 +7,9 @@ import { TaskDetailModal } from "./taskDetailModal"; export function TaskCard({ task, deleteTask, updateTask }) { const [mouseIsOver, setMouseIsOver] = useState(false); + // console.log(task.challenge); + // console.log(task.importance); + // console.log(task.difficulty); const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({ id: task.id, @@ -14,12 +18,13 @@ export function TaskCard({ task, deleteTask, updateTask }) { task, }, }); - const style = { transition, transform: CSS.Transform.toString(transform), }; + + { /* If card is dragged */ } @@ -60,7 +65,7 @@ export function TaskCard({ task, deleteTask, updateTask }) { setMouseIsOver(false); }}>

document.getElementById(`task_detail_modal_${task.id}`).showModal()}> {task.content}

@@ -77,4 +82,4 @@ export function TaskCard({ task, deleteTask, updateTask }) {
); -} +} \ No newline at end of file diff --git a/frontend/src/components/kanbanBoard/taskDetailModal.jsx b/frontend/src/components/kanbanBoard/taskDetailModal.jsx index e151d8f..d349c3d 100644 --- a/frontend/src/components/kanbanBoard/taskDetailModal.jsx +++ b/frontend/src/components/kanbanBoard/taskDetailModal.jsx @@ -7,6 +7,9 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng const [isChallengeChecked, setChallengeChecked] = useState(challenge); const [isImportantChecked, setImportantChecked] = useState(importance); const [currentDifficulty, setCurrentDifficulty] = useState(difficulty); + // console.log(currentDifficulty); + // console.log(isChallengeChecked); + // console.log(isImportantChecked); const handleChallengeChange = () => { setChallengeChecked(!isChallengeChecked); From 6d0ac33d45e9d1f7d76ca9fc101a8293ab5828cb Mon Sep 17 00:00:00 2001 From: Pattadon Date: Sun, 26 Nov 2023 01:40:06 +0700 Subject: [PATCH 09/45] Fix order of donut chart data --- frontend/src/components/dashboard/PieChart.jsx | 4 ++-- frontend/src/components/kanbanBoard/taskCard.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/dashboard/PieChart.jsx b/frontend/src/components/dashboard/PieChart.jsx index 645137b..ca825bc 100644 --- a/frontend/src/components/dashboard/PieChart.jsx +++ b/frontend/src/components/dashboard/PieChart.jsx @@ -13,8 +13,8 @@ export function DonutChartGraph() { const completedTask = response.data.total_completed_tasks || 0; const donutData = [ - { name: "Completed task", count: totalTask }, - { name: "Total task", count: completedTask }, + { name: "Completed task", count: completedTask }, + { name: "Total task", count: totalTask }, ]; setDonutData(donutData); diff --git a/frontend/src/components/kanbanBoard/taskCard.jsx b/frontend/src/components/kanbanBoard/taskCard.jsx index 1a151c8..86a2036 100644 --- a/frontend/src/components/kanbanBoard/taskCard.jsx +++ b/frontend/src/components/kanbanBoard/taskCard.jsx @@ -60,7 +60,7 @@ export function TaskCard({ task, deleteTask, updateTask }) { setMouseIsOver(false); }}>

document.getElementById(`task_detail_modal_${task.id}`).showModal()}> {task.content}

From bd946bce9401ab9879f30665e6174c63b8aff927 Mon Sep 17 00:00:00 2001 From: Pattadon Date: Sun, 26 Nov 2023 01:41:55 +0700 Subject: [PATCH 10/45] Fix order of donut chart data --- frontend/src/components/dashboard/PieChart.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/dashboard/PieChart.jsx b/frontend/src/components/dashboard/PieChart.jsx index 645137b..c2b94c4 100644 --- a/frontend/src/components/dashboard/PieChart.jsx +++ b/frontend/src/components/dashboard/PieChart.jsx @@ -13,8 +13,8 @@ export function DonutChartGraph() { const completedTask = response.data.total_completed_tasks || 0; const donutData = [ - { name: "Completed task", count: totalTask }, - { name: "Total task", count: completedTask }, + { name: "Completed task", count: completedTask}, + { name: "Total task", count: totalTask }, ]; setDonutData(donutData); From 5c9319c5f133071f3c1cad9172834d5a2199a4df Mon Sep 17 00:00:00 2001 From: sosokker Date: Sun, 26 Nov 2023 09:25:50 +0700 Subject: [PATCH 11/45] Move user creation related signal to user.signals --- backend/boards/signals.py | 17 ---------- backend/tasks/signals.py | 67 ++------------------------------------- backend/users/signals.py | 67 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 83 deletions(-) delete mode 100644 backend/boards/signals.py diff --git a/backend/boards/signals.py b/backend/boards/signals.py deleted file mode 100644 index 2c44daa..0000000 --- a/backend/boards/signals.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.db.models.signals import post_save -from django.dispatch import receiver - -from boards.models import Board, ListBoard -from users.models import CustomUser - -@receiver(post_save, sender=CustomUser) -def create_default_board(sender, instance, created, **kwargs): - """Signal handler to automatically create a default Board for a user upon creation.""" - if created: - # Create unique board by user id - user_id = instance.id - board = Board.objects.create(user=instance, name=f"Board of #{user_id}") - ListBoard.objects.create(board=board, name="Backlog", position=1) - ListBoard.objects.create(board=board, name="Doing", position=2) - ListBoard.objects.create(board=board, name="Review", position=3) - ListBoard.objects.create(board=board, name="Done", position=4) \ No newline at end of file diff --git a/backend/tasks/signals.py b/backend/tasks/signals.py index 4c9e6f2..f7436aa 100644 --- a/backend/tasks/signals.py +++ b/backend/tasks/signals.py @@ -1,8 +1,7 @@ -from django.db.models.signals import pre_save, post_save +from django.db.models.signals import pre_save from django.dispatch import receiver from django.utils import timezone -from boards.models import ListBoard, Board from tasks.models import Todo @@ -24,66 +23,4 @@ def update_priority(sender, instance, **kwargs): elif time_until_due <= urgency_threshold and instance.importance < importance_threshold: instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT else: - instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT - - -# @receiver(post_save, sender=Todo) -# def assign_todo_to_listboard(sender, instance, created, **kwargs): -# """Signal handler to automatically assign a Todo to the first ListBoard in the user's Board upon creation.""" -# if created: -# user_board = instance.user.board_set.first() - -# if user_board: -# first_list_board = user_board.listboard_set.order_by('position').first() - -# if first_list_board: -# instance.list_board = first_list_board -# instance.save() - - -@receiver(post_save, sender=ListBoard) -def create_placeholder_tasks(sender, instance, created, **kwargs): - """ - Signal handler to create placeholder tasks for each ListBoard. - """ - if created: - list_board_position = instance.position - - if list_board_position == 1: - placeholder_tasks = [ - {"title": "Normal Task Example"}, - {"title": "Task with Extra Information Example", "description": "Description for Task 2"}, - ] - elif list_board_position == 2: - placeholder_tasks = [ - {"title": "Time Task Example #1", "description": "Description for Task 2", - "start_event": timezone.now(), "end_event": timezone.now() + timezone.timedelta(days=5)}, - ] - elif list_board_position == 3: - placeholder_tasks = [ - {"title": "Time Task Example #2", "description": "Description for Task 2", - "start_event": timezone.now(), "end_event": timezone.now() + timezone.timedelta(days=30)}, - ] - elif list_board_position == 4: - placeholder_tasks = [ - {"title": "Completed Task Example", "description": "Description for Task 2", - "start_event": timezone.now(), "completed": True}, - ] - else: - placeholder_tasks = [ - {"title": "Default Task Example"}, - ] - - for task_data in placeholder_tasks: - Todo.objects.create( - list_board=instance, - user=instance.board.user, - title=task_data["title"], - notes=task_data.get("description", ""), - is_active=True, - start_event=task_data.get("start_event"), - end_event=task_data.get("end_event"), - completed=task_data.get("completed", False), - creation_date=timezone.now(), - last_update=timezone.now(), - ) \ No newline at end of file + instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT \ No newline at end of file diff --git a/backend/users/signals.py b/backend/users/signals.py index 817986b..22599b1 100644 --- a/backend/users/signals.py +++ b/backend/users/signals.py @@ -1,9 +1,74 @@ +from django.utils import timezone from django.db.models.signals import post_save from django.dispatch import receiver +from tasks.models import Todo from users.models import CustomUser, UserStats +from boards.models import ListBoard, Board + @receiver(post_save, sender=CustomUser) def create_user_stats(sender, instance, created, **kwargs): if created: - UserStats.objects.create(user=instance) \ No newline at end of file + UserStats.objects.create(user=instance) + + +@receiver(post_save, sender=CustomUser) +def create_default_board(sender, instance, created, **kwargs): + """Signal handler to automatically create a default Board for a user upon creation.""" + if created: + # Create unique board by user id + user_id = instance.id + board = Board.objects.create(user=instance, name=f"Board of #{user_id}") + ListBoard.objects.create(board=board, name="Backlog", position=1) + ListBoard.objects.create(board=board, name="Doing", position=2) + ListBoard.objects.create(board=board, name="Review", position=3) + ListBoard.objects.create(board=board, name="Done", position=4) + + +@receiver(post_save, sender=ListBoard) +def create_placeholder_tasks(sender, instance, created, **kwargs): + """ + Signal handler to create placeholder tasks for each ListBoard. + """ + if created: + list_board_position = instance.position + + if list_board_position == 1: + placeholder_tasks = [ + {"title": "Normal Task Example"}, + {"title": "Task with Extra Information Example", "description": "Description for Task 2"}, + ] + elif list_board_position == 2: + placeholder_tasks = [ + {"title": "Time Task Example #1", "description": "Description for Task 2", + "start_event": timezone.now(), "end_event": timezone.now() + timezone.timedelta(days=5)}, + ] + elif list_board_position == 3: + placeholder_tasks = [ + {"title": "Time Task Example #2", "description": "Description for Task 2", + "start_event": timezone.now(), "end_event": timezone.now() + timezone.timedelta(days=30)}, + ] + elif list_board_position == 4: + placeholder_tasks = [ + {"title": "Completed Task Example", "description": "Description for Task 2", + "start_event": timezone.now(), "completed": True}, + ] + else: + placeholder_tasks = [ + {"title": "Default Task Example"}, + ] + + for task_data in placeholder_tasks: + Todo.objects.create( + list_board=instance, + user=instance.board.user, + title=task_data["title"], + notes=task_data.get("description", ""), + is_active=True, + start_event=task_data.get("start_event"), + end_event=task_data.get("end_event"), + completed=task_data.get("completed", False), + creation_date=timezone.now(), + last_update=timezone.now(), + ) \ No newline at end of file From 41cf2fb6d3a57e127aa99bec0bde482fa9469a81 Mon Sep 17 00:00:00 2001 From: sosokker Date: Sun, 26 Nov 2023 10:10:45 +0700 Subject: [PATCH 12/45] Add user creation signal test and Spectacular need Authentication --- backend/boards/apps.py | 5 +--- backend/core/local_settings.py | 1 + backend/core/production_settings.py | 1 + backend/users/tests.py | 40 +++++++++++++++++++++++++++-- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/backend/boards/apps.py b/backend/boards/apps.py index d10d8fa..cdcb8cf 100644 --- a/backend/boards/apps.py +++ b/backend/boards/apps.py @@ -3,7 +3,4 @@ from django.apps import AppConfig class BoardsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'boards' - - def ready(self): - import boards.signals \ No newline at end of file + name = 'boards' \ No newline at end of file diff --git a/backend/core/local_settings.py b/backend/core/local_settings.py index 9e7903c..d801d36 100644 --- a/backend/core/local_settings.py +++ b/backend/core/local_settings.py @@ -88,6 +88,7 @@ SPECTACULAR_SETTINGS = { 'DESCRIPTION': 'API documentation for TurTask', 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, + 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'], } REST_USE_JWT = True diff --git a/backend/core/production_settings.py b/backend/core/production_settings.py index dd9eeb3..c23cdb4 100644 --- a/backend/core/production_settings.py +++ b/backend/core/production_settings.py @@ -88,6 +88,7 @@ SPECTACULAR_SETTINGS = { 'DESCRIPTION': 'API documentation for TurTask', 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, + 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'], } REST_USE_JWT = True diff --git a/backend/users/tests.py b/backend/users/tests.py index 7ce503c..5f9bdd6 100644 --- a/backend/users/tests.py +++ b/backend/users/tests.py @@ -1,3 +1,39 @@ -from django.test import TestCase +from rest_framework.test import APITestCase +from rest_framework import status +from django.urls import reverse +from users.models import CustomUser, UserStats +from boards.models import Board, ListBoard +from tasks.models import Todo -# Create your tests here. +class SignalsTest(APITestCase): + def setUp(self): + response = self.client.post(reverse('create_user'), {'email': 'testusertestuser123@mail.com', + 'username': 'testusertestuser123', + 'password': '321testpassword123'}) + # force login If response is 201 OK + if response.status_code == status.HTTP_201_CREATED: + self.user = CustomUser.objects.get(username='testusertestuser123') + self.client.force_login(self.user) + + def test_create_user_with_stas_default_boards_and_lists(self): + # Stats check + self.assertTrue(UserStats.objects.filter(user=self.user).exists()) + + # check if user is created + self.assertEqual(CustomUser.objects.count(), 1) + user = CustomUser.objects.get(username='testusertestuser123') + + # Check for default board + self.assertEqual(Board.objects.filter(user=self.user).count(), 1) + + # Check for default lists in board + default_board = Board.objects.get(user=self.user) + self.assertEqual(ListBoard.objects.filter(board=default_board).count(), 4) + + def test_create_user_with_placeholder_tasks(self): + default_board = Board.objects.get(user=self.user) + + # Check if placeholder tasks are created for each ListBoard + for list_board in ListBoard.objects.filter(board=default_board): + placeholder_tasks_count = Todo.objects.filter(list_board=list_board).count() + self.assertTrue(placeholder_tasks_count > 0) \ No newline at end of file From 80ad7fc74437a4790c08a80904b9f57c6bcb7a83 Mon Sep 17 00:00:00 2001 From: sosokker Date: Sun, 26 Nov 2023 11:46:50 +0700 Subject: [PATCH 13/45] Update Tasks Test --- backend/tasks/tests/test_deserializer.py | 14 ++- backend/tasks/tests/test_todo_creation.py | 124 ++++++++++---------- backend/tasks/tests/test_todo_eisenhower.py | 15 ++- backend/tasks/tests/utils.py | 35 +++--- 4 files changed, 95 insertions(+), 93 deletions(-) diff --git a/backend/tasks/tests/test_deserializer.py b/backend/tasks/tests/test_deserializer.py index 306185c..758fdd1 100644 --- a/backend/tasks/tests/test_deserializer.py +++ b/backend/tasks/tests/test_deserializer.py @@ -1,18 +1,22 @@ from datetime import datetime from zoneinfo import ZoneInfo -from django.test import TestCase +from rest_framework.test import APITestCase + from django.utils import timezone -from tasks.tests.utils import create_test_user, login_user +from tasks.tests.utils import create_test_user from tasks.serializers import TodoUpdateSerializer from tasks.models import Todo +from boards.models import Board -class TaskUpdateSerializerTest(TestCase): +class TaskUpdateSerializerTest(APITestCase): def setUp(self): self.user = create_test_user() + self.client.force_authenticate(user=self.user) self.current_time = '2020-08-01T00:00:00Z' self.end_time = '2020-08-01T00:00:00Z' + self.list_board = Board.objects.get(user=self.user).listboard_set.first() def test_serializer_create(self): data = { @@ -23,6 +27,7 @@ class TaskUpdateSerializerTest(TestCase): 'updated': self.end_time, 'start_datetime' : self.current_time, 'end_datetie': self.end_time, + 'list_board': self.list_board.id, } serializer = TodoUpdateSerializer(data=data, user=self.user) @@ -32,7 +37,7 @@ class TaskUpdateSerializerTest(TestCase): self.assertIsInstance(task, Todo) def test_serializer_update(self): - task = Todo.objects.create(title='Original Task', notes='Original description', user=self.user) + task = Todo.objects.create(title='Original Task', notes='Original description', user=self.user, list_board=self.list_board) data = { 'id': '32141cwaNcapufh8jq2conw', @@ -42,6 +47,7 @@ class TaskUpdateSerializerTest(TestCase): 'updated': self.end_time, 'start_datetime' : self.current_time, 'end_datetie': self.end_time, + 'list_board': self.list_board.id, } serializer = TodoUpdateSerializer(instance=task, data=data) diff --git a/backend/tasks/tests/test_todo_creation.py b/backend/tasks/tests/test_todo_creation.py index 7c66724..cc7dfc4 100644 --- a/backend/tasks/tests/test_todo_creation.py +++ b/backend/tasks/tests/test_todo_creation.py @@ -2,72 +2,72 @@ from datetime import datetime, timedelta from django.urls import reverse from rest_framework import status from rest_framework.test import APITestCase -from tasks.tests.utils import create_test_user, login_user +from tasks.tests.utils import create_test_user from tasks.models import Todo +from boards.models import ListBoard, Board -# class TodoViewSetTests(APITestCase): -# def setUp(self): -# self.user = create_test_user() -# self.client = login_user(self.user) -# self.url = reverse("todo-list") -# self.due_date = datetime.now() + timedelta(days=5) +class TodoViewSetTests(APITestCase): + def setUp(self): + self.user = create_test_user() + self.client.force_authenticate(user=self.user) + self.url = reverse("todo-list") + self.due_date = datetime.now() + timedelta(days=5) + self.list_board = Board.objects.get(user=self.user).listboard_set.first() -# def test_create_valid_todo(self): -# """ -# Test creating a valid task using the API. -# """ -# data = { -# 'title': 'Test Task', -# 'type': 'habit', -# 'exp': 10, -# 'attribute': 'str', -# 'priority': 1, -# 'difficulty': 1, -# 'user': self.user.id, -# 'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'), -# } -# response = self.client.post(self.url, data, format='json') -# self.assertEqual(response.status_code, status.HTTP_201_CREATED) -# self.assertEqual(Todo.objects.count(), 1) -# self.assertEqual(Todo.objects.get().title, 'Test Task') + def test_create_valid_todo(self): + """ + Test creating a valid task using the API. + """ + data = { + 'title': 'Test Task', + 'type': 'habit', + 'difficulty': 1, + 'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'), + 'list_board': self.list_board.id, + } + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Todo.objects.count(), 1) + self.assertEqual(Todo.objects.get().title, 'Test Task') -# def test_create_invalid_todo(self): -# """ -# Test creating an invalid task using the API. -# """ -# data = { -# 'type': 'invalid', # Invalid task type -# } -# response = self.client.post(self.url, data, format='json') -# self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) -# self.assertEqual(Todo.objects.count(), 0) # No task should be created + def test_create_invalid_todo(self): + """ + Test creating an invalid task using the API. + """ + data = { + 'type': 'invalid', # Invalid task type + } + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) + self.assertEqual(Todo.objects.count(), 0) # No task should be created -# def test_missing_required_fields(self): -# """ -# Test creating a task with missing required fields using the API. -# """ -# data = { -# 'title': 'Incomplete Task', -# 'type': 'habit', -# } -# response = self.client.post(self.url, data, format='json') -# self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) -# self.assertEqual(Todo.objects.count(), 0) # No task should be created + def test_missing_required_fields(self): + """ + Test creating a task with missing required fields using the API. + """ + data = { + 'type': 'habit', + } + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) + self.assertEqual(Todo.objects.count(), 0) # No task should be created -# def test_invalid_user_id(self): -# """ -# Test creating a task with an invalid user ID using the API. -# """ -# data = { -# 'title': 'Test Task', -# 'type': 'habit', -# 'exp': 10, -# 'priority': 1, -# 'difficulty': 1, -# 'user': 999, # Invalid user ID -# 'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'), -# } -# response = self.client.post(self.url, data, format='json') -# self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) -# self.assertEqual(Todo.objects.count(), 0) # No task should be created + def test_invalid_user_id(self): + """ + Test creating a task with an invalid user ID using the API (OK because we retreive) + id from request. + """ + data = { + 'title': 'Test Task', + 'type': 'habit', + 'exp': 10, + 'priority': 1, + 'difficulty': 1, + 'user': -100, # Invalid user ID + 'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'), + 'list_board': self.list_board.id, + } + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Todo.objects.count(), 1) # No task should be created diff --git a/backend/tasks/tests/test_todo_eisenhower.py b/backend/tasks/tests/test_todo_eisenhower.py index 41ee078..2d14877 100644 --- a/backend/tasks/tests/test_todo_eisenhower.py +++ b/backend/tasks/tests/test_todo_eisenhower.py @@ -1,36 +1,39 @@ from datetime import datetime, timedelta, timezone -from django.test import TestCase +from rest_framework.test import APITestCase from tasks.models import Todo from tasks.tests.utils import create_test_user +from boards.models import Board -class TodoPriorityTest(TestCase): +class TodoPriorityTest(APITestCase): def setUp(self): self.user = create_test_user() + self.client.force_authenticate(user=self.user) + self.list_board = Board.objects.get(user=self.user).listboard_set.first() def test_priority_calculation(self): # Important = 2, Till Due = none - todo = Todo(importance=2, end_event=None, user=self.user) + todo = Todo(importance=2, end_event=None, user=self.user, list_board=self.list_board) todo.save() # 'Not Important & Not Urgent' self.assertEqual(todo.priority, Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT) due_date = datetime.now(timezone.utc) + timedelta(days=1) # Important = 4, Till Due = 1 - todo = Todo(importance=4, end_event=due_date, user=self.user) + todo = Todo(importance=4, end_event=due_date, user=self.user, list_board=self.list_board) todo.save() # 'Important & Urgent' self.assertEqual(todo.priority, Todo.EisenhowerMatrix.IMPORTANT_URGENT) due_date = datetime.now(timezone.utc) + timedelta(days=10) # Important = 3, Till Due = 10 - todo = Todo(importance=3, end_event=due_date, user=self.user) + todo = Todo(importance=3, end_event=due_date, user=self.user, list_board=self.list_board) todo.save() # 'Important & Not Urgent' self.assertEqual(todo.priority, Todo.EisenhowerMatrix.IMPORTANT_NOT_URGENT) due_date = datetime.now(timezone.utc) + timedelta(days=2) # Important = 1, Till Due = 2 - todo = Todo(importance=1, end_event=due_date, user=self.user) + todo = Todo(importance=1, end_event=due_date, user=self.user, list_board=self.list_board) todo.save() # 'Not Important & Urgent' self.assertEqual(todo.priority, Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT) diff --git a/backend/tasks/tests/utils.py b/backend/tasks/tests/utils.py index b1bef0b..663cd0a 100644 --- a/backend/tasks/tests/utils.py +++ b/backend/tasks/tests/utils.py @@ -1,26 +1,24 @@ +from rest_framework import status from rest_framework.test import APIClient +from django.urls import reverse from users.models import CustomUser from ..models import Todo -def create_test_user(email="testusertestuser@example.com", username="testusertestuser", - first_name="Test", password="testpassword",): - """create predifined user for testing""" - return CustomUser.objects.create_user( - email=email, - username=username, - first_name=first_name, - password=password, - ) - - -def login_user(user): - """Login a user to API client.""" - +def create_test_user(email="testusertestuser@example.com", + username="testusertestuser", + password="testpassword",) -> CustomUser: + """create predifined user without placeholder task for testing""" client = APIClient() - client.force_authenticate(user=user) - return client + response = client.post(reverse('create_user'), {'email': email, + 'username': username, + 'password': password}) + if response.status_code == status.HTTP_201_CREATED: + user = CustomUser.objects.get(username='testusertestuser') + user.todo_set.all().delete() + return user + return None def create_task_json(user, **kwargs): @@ -29,10 +27,7 @@ def create_task_json(user, **kwargs): "title": "Test Task", "type": "habit", "notes": "This is a test task created via the API.", - "exp": 10, - "priority": 1.5, "difficulty": 1, - "attribute": "str", "challenge": False, "fromSystem": False, "creation_date": None, @@ -51,8 +46,6 @@ def create_test_task(user, **kwargs): 'title': "Test Task", 'task_type': 'habit', 'notes': "This is a test task created via the API.", - 'exp': 10, - 'priority': 1.5, 'difficulty': 1, 'attribute': 'str', 'challenge': False, From b36c675084a6a8c8668dc5789dfaeade1bb169fa Mon Sep 17 00:00:00 2001 From: sosokker Date: Sun, 26 Nov 2023 11:47:15 +0700 Subject: [PATCH 14/45] Task serializer must test userid availability --- backend/tasks/tasks/serializers.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/tasks/tasks/serializers.py b/backend/tasks/tasks/serializers.py index d2863a0..12bea72 100644 --- a/backend/tasks/tasks/serializers.py +++ b/backend/tasks/tasks/serializers.py @@ -1,4 +1,5 @@ from rest_framework import serializers +from users.models import CustomUser from boards.models import ListBoard from tasks.models import Todo, RecurrenceTask, Habit @@ -8,7 +9,14 @@ class TaskSerializer(serializers.ModelSerializer): fields = '__all__' def create(self, validated_data): - # Create a new task with validated data + user_id = validated_data.get('user') + + try: + user = CustomUser.objects.get(id=user_id) + except CustomUser.DoesNotExist: + raise serializers.ValidationError("User with the provided ID does not exist.") + + validated_data['user'] = user return Todo.objects.create(**validated_data) class TaskCreateSerializer(serializers.ModelSerializer): From 07b8b419e7944f9a0cb855c7f8e32334e0ea9a89 Mon Sep 17 00:00:00 2001 From: sosokker Date: Sun, 26 Nov 2023 11:47:50 +0700 Subject: [PATCH 15/45] Task from google will assign to first list of user --- backend/tasks/serializers.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/tasks/serializers.py b/backend/tasks/serializers.py index a48330e..9d447e1 100644 --- a/backend/tasks/serializers.py +++ b/backend/tasks/serializers.py @@ -1,5 +1,6 @@ from rest_framework import serializers -from .models import Todo, RecurrenceTask +from boards.models import Board +from tasks.models import Todo, RecurrenceTask class GoogleCalendarEventSerializer(serializers.Serializer): @@ -17,16 +18,19 @@ class TodoUpdateSerializer(serializers.ModelSerializer): updated = serializers.DateTimeField(source="last_update") start_datetime = serializers.DateTimeField(source="start_event", required=False) end_datetime = serializers.DateTimeField(source="end_event", required=False) - + list_board = serializers.SerializerMethodField() class Meta: model = Todo - fields = ('id', 'summary', 'description', 'created', 'updated', 'start_datetime', 'end_datetime') + fields = ('id', 'summary', 'description', 'created', 'updated', 'start_datetime', 'end_datetime', 'list_board') def __init__(self, *args, **kwargs): self.user = kwargs.pop('user', None) super(TodoUpdateSerializer, self).__init__(*args, **kwargs) + def get_list_board(self, obj): + return Board.objects.filter(user=self.user).first() + def create(self, validated_data): validated_data['user'] = self.user task = Todo.objects.create(**validated_data) From e93667b6974aa09a1ccbb0dce74ef2ea930b2408 Mon Sep 17 00:00:00 2001 From: sosokker Date: Sun, 26 Nov 2023 11:51:33 +0700 Subject: [PATCH 16/45] Update test of dashboard --- backend/dashboard/tests.py | 89 +++++++------------------------------- 1 file changed, 16 insertions(+), 73 deletions(-) diff --git a/backend/dashboard/tests.py b/backend/dashboard/tests.py index 943c3de..9d20ab4 100644 --- a/backend/dashboard/tests.py +++ b/backend/dashboard/tests.py @@ -1,32 +1,35 @@ -from django.test import TestCase +from rest_framework.test import APITestCase from django.urls import reverse from tasks.models import Todo from django.utils import timezone from datetime import timedelta -from tasks.tests.utils import create_test_user, login_user +from boards.models import Board +from tasks.tests.utils import create_test_user -class DashboardStatsAndWeeklyViewSetTests(TestCase): +class DashboardStatsAndWeeklyViewSetTests(APITestCase): def setUp(self): self.user = create_test_user() - self.client = login_user(self.user) + self.client.force_authenticate(user=self.user) + self.list_board = Board.objects.get(user=self.user).listboard_set.first() - def create_task(self, title, completed=False, completion_date=None, end_event=None): + def _create_task(self, title, completed=False, completion_date=None, end_event=None): return Todo.objects.create( user=self.user, title=title, completed=completed, completion_date=completion_date, - end_event=end_event + end_event=end_event, + list_board=self.list_board ) def test_dashboard_stats_view(self): # Create tasks for testing - self.create_task('Task 1', completed=True) - self.create_task('Task 2', end_event=timezone.now() - timedelta(days=8)) - self.create_task('Task 3', end_event=timezone.now()) + self._create_task('Task 1', completed=True) + self._create_task('Task 2', end_event=timezone.now() - timedelta(days=8)) + self._create_task('Task 3', end_event=timezone.now()) - response = self.client.get(reverse('stats-list')) + response = self.client.get(reverse('statstodo-list')) self.assertEqual(response.status_code, 200) self.assertEqual(response.data['completed_this_week'], 1) @@ -35,69 +38,9 @@ class DashboardStatsAndWeeklyViewSetTests(TestCase): def test_dashboard_weekly_view(self): # Create tasks for testing - self.create_task('Task 1', completion_date=timezone.now() - timedelta(days=1)) - self.create_task('Task 2', end_event=timezone.now() - timedelta(days=8)) - self.create_task('Task 3', end_event=timezone.now()) + self._create_task('Task 1', completion_date=timezone.now() - timedelta(days=1)) + self._create_task('Task 2', end_event=timezone.now() - timedelta(days=8)) + self._create_task('Task 3', end_event=timezone.now()) response = self.client.get(reverse('weekly-list')) self.assertEqual(response.status_code, 200) - - -# class DashboardStatsAPITestCase(TestCase): -# def setUp(self): -# # Create a test user -# self.user = create_test_user() - -# # Create test tasks -# self.todo = Todo.objects.create(user=self.user, title='Test Todo') -# self.recurrence_task = RecurrenceTask.objects.create(user=self.user, title='Test Recurrence Task') - -# # Create an API client -# self.client = APIClient() - -# def test_dashboard_stats_api(self): -# # Authenticate the user -# self.client.force_authenticate(user=self.user) - -# # Make a GET request to the DashboardStatsAPIView -# response = self.client.get(reverse("dashboard-stats")) - -# # Assert the response status code is 200 -# self.assertEqual(response.status_code, 200) - -# def test_task_completion_status_update(self): -# # Authenticate the user -# self.client.force_authenticate(user=self.user) - -# # Make a POST request to update the completion status of a task -# data = {'task_id': self.todo.id, 'is_completed': True} -# response = self.client.post(reverse("dashboard-stats"), data, format='json') - -# # Assert the response status code is 200 -# self.assertEqual(response.status_code, 200) - -# # Assert the message in the response -# self.assertEqual(response.data['message'], 'Task completion status updated successfully') - -# # Refresh the todo instance from the database and assert the completion status -# self.todo.refresh_from_db() -# self.assertTrue(self.todo.completed) - - -# class WeeklyStatsAPITestCase(TestCase): -# def setUp(self): -# # Create a test user -# self.user = create_test_user() - -# # Create an API client -# self.client = APIClient() - -# def test_weekly_stats_api(self): -# # Authenticate the user -# self.client.force_authenticate(user=self.user) - -# # Make a GET request to the WeeklyStatsAPIView -# response = self.client.get(reverse('dashboard-weekly-stats')) - -# # Assert the response status code is 200 -# self.assertEqual(response.status_code, 200) From 976c786870ce8f055a6ea739b779381b60132b33 Mon Sep 17 00:00:00 2001 From: sosokker Date: Sun, 26 Nov 2023 11:57:05 +0700 Subject: [PATCH 17/45] Fix serializer can't get user first list --- backend/tasks/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/tasks/serializers.py b/backend/tasks/serializers.py index 9d447e1..cf2046c 100644 --- a/backend/tasks/serializers.py +++ b/backend/tasks/serializers.py @@ -29,10 +29,11 @@ class TodoUpdateSerializer(serializers.ModelSerializer): super(TodoUpdateSerializer, self).__init__(*args, **kwargs) def get_list_board(self, obj): - return Board.objects.filter(user=self.user).first() + return Board.objects.get(user=self.user).listboard_set.first() def create(self, validated_data): validated_data['user'] = self.user + validated_data['list_board'] = self.get_list_board(self) task = Todo.objects.create(**validated_data) return task From d1fca5c7b3900aeeb84fba9a445159d2f27c6070 Mon Sep 17 00:00:00 2001 From: sosokker Date: Sun, 26 Nov 2023 11:59:58 +0700 Subject: [PATCH 18/45] Fix typo in dashboard import path --- frontend/src/App.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 625b9b8..a917ca5 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -11,7 +11,7 @@ import { SideNav } from "./components/navigations/IconSideNav"; import { Eisenhower } from "./components/EisenhowerMatrix/Eisenhower"; import { PrivateRoute } from "./PrivateRoute"; import { ProfileUpdatePage } from "./components/profile/profilePage"; -import { Dashboard } from "./components/dashboard/Dashboard"; +import { Dashboard } from "./components/dashboard/dashboard"; import { LandingPage } from "./components/landingPage/LandingPage"; import { PublicRoute } from "./PublicRoute"; import { useAuth } from "./hooks/AuthHooks"; From 4a3f253e3049f97ef4479dd423642897a56e13fc Mon Sep 17 00:00:00 2001 From: sosokker Date: Sun, 26 Nov 2023 22:28:39 +0700 Subject: [PATCH 19/45] Add placeholder for ui to provide more detail on task card --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 47 ++++++ .../src/components/kanbanBoard/taskCard.jsx | 147 ++++++++++++++---- .../kanbanBoard/taskDetailModal.jsx | 142 +++++++++++++++-- 4 files changed, 290 insertions(+), 47 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index f057dce..44e4b66 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,6 +42,7 @@ "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^2.9.1", + "react-datepicker": "^4.23.0", "react-datetime-picker": "^5.5.3", "react-dom": "^18.2.0", "react-icons": "^4.11.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 22fdadd..2bb6ef0 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -101,6 +101,9 @@ dependencies: react-bootstrap: specifier: ^2.9.1 version: 2.9.1(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + react-datepicker: + specifier: ^4.23.0 + version: 4.23.0(react-dom@18.2.0)(react@18.2.0) react-datetime-picker: specifier: ^5.5.3 version: 5.5.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) @@ -3473,6 +3476,22 @@ packages: - '@types/react-dom' dev: false + /react-datepicker@4.23.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-w+msqlOZ14v6H1UknTKtZw/dw9naFMgAOspf59eY130gWpvy5dvKj/bgsFICDdvxB7PtKWxDcbGlAqCloY1d2A==} + peerDependencies: + react: ^16.9.0 || ^17 || ^18 + react-dom: ^16.9.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.11.8 + classnames: 2.3.2 + date-fns: 2.30.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-onclickoutside: 6.13.0(react-dom@18.2.0)(react@18.2.0) + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) + dev: false + /react-datetime-picker@5.5.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-bWGEPwGrZjaXTB8P4pbTSDygctLaqTWp0nNibaz8po+l4eTh9gv3yiJ+n4NIcpIJDqZaQJO57Bnij2rAFVQyLw==} peerDependencies: @@ -3520,6 +3539,10 @@ packages: scheduler: 0.23.0 dev: false + /react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + dev: false + /react-fit@1.7.1(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-y/TYovCCBzfIwRJsbLj0rH4Es40wPQhU5GPPq9GlbdF09b0OdzTdMSkBza0QixSlgFzTm6dkM7oTFzaVvaBx+w==} peerDependencies: @@ -3565,6 +3588,30 @@ packages: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} dev: false + /react-onclickoutside@6.13.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==} + peerDependencies: + react: ^15.5.x || ^16.x || ^17.x || ^18.x + react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.11.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-fast-compare: 3.2.2 + warning: 4.0.3 + dev: false + /react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} peerDependencies: diff --git a/frontend/src/components/kanbanBoard/taskCard.jsx b/frontend/src/components/kanbanBoard/taskCard.jsx index e631a26..4858d82 100644 --- a/frontend/src/components/kanbanBoard/taskCard.jsx +++ b/frontend/src/components/kanbanBoard/taskCard.jsx @@ -1,16 +1,14 @@ import { useState } from "react"; -import { useEffect } from "react"; -import { BsFillTrashFill } from "react-icons/bs"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { TaskDetailModal } from "./taskDetailModal"; +import { GoChecklist, GoArchive } from "react-icons/go"; export function TaskCard({ task, deleteTask, updateTask }) { + // State to track if the mouse is over the task card const [mouseIsOver, setMouseIsOver] = useState(false); - // console.log(task.challenge); - // console.log(task.importance); - // console.log(task.difficulty); + // DnD Kit hook for sortable items const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({ id: task.id, data: { @@ -18,68 +16,157 @@ export function TaskCard({ task, deleteTask, updateTask }) { task, }, }); + + // Style for the task card, adjusting for dragging animation const style = { transition, transform: CSS.Transform.toString(transform), }; + // ---- DESC AND TAG ---- */ + // Tags + const tags = + task.tags.length > 0 ? ( +
+ {task.tags.map((tag, index) => ( +
+ {tag.label} +
+ ))} +
+ ) : null; - { - /* If card is dragged */ - } + // difficulty? + const difficultyTag = task.difficulty ? ( + + difficulty + + ) : null; + + // Due Date + const dueDateTag = + task.end_event && new Date(task.end_event) > new Date() + ? (() => { + const daysUntilDue = Math.ceil((new Date(task.end_event) - new Date()) / (1000 * 60 * 60 * 24)); + + let colorClass = + daysUntilDue >= 365 + ? "gray-200" + : daysUntilDue >= 30 + ? "blue-200" + : daysUntilDue >= 7 + ? "green-200" + : daysUntilDue > 0 + ? "yellow-200" + : "red-200"; + + const formattedDueDate = + daysUntilDue >= 365 + ? new Date(task.end_event).toLocaleDateString("en-US", { + day: "numeric", + month: "short", + year: "numeric", + }) + : new Date(task.end_event).toLocaleDateString("en-US", { day: "numeric", month: "short" }); + + return ( + + Due: {formattedDueDate} + + ); + })() + : null; + + // Subtask count + const subtaskCountTag = task.subtaskCount ? ( + + {task.subtaskCount} + + ) : null; + + // ---- DRAG STATE ---- */ + + // If the card is being dragged if (isDragging) { return (
); } + // If the card is not being dragged return (
+ {/* Task Detail Modal */} + + {/* -------- Task Card -------- */}
{ setMouseIsOver(true); }} onMouseLeave={() => { setMouseIsOver(false); }}> -

document.getElementById(`task_detail_modal_${task.id}`).showModal()}> - {task.content} -

- - {mouseIsOver && ( - - )} + {/* -------- Task Content -------- */} + {/* Tags */} + {tags} +
+ {/* Title */} +

document.getElementById(`task_detail_modal_${task.id}`).showModal()}> + {task.content} +

+ {/* -------- Archive Task Button -------- */} + {mouseIsOver && ( + + )} +
+ {/* Description */} +
+ {difficultyTag} + {dueDateTag} + {subtaskCountTag} +
); -} \ No newline at end of file +} diff --git a/frontend/src/components/kanbanBoard/taskDetailModal.jsx b/frontend/src/components/kanbanBoard/taskDetailModal.jsx index d349c3d..e4317e5 100644 --- a/frontend/src/components/kanbanBoard/taskDetailModal.jsx +++ b/frontend/src/components/kanbanBoard/taskDetailModal.jsx @@ -2,14 +2,19 @@ import { useState } from "react"; import { FaTasks, FaRegListAlt } from "react-icons/fa"; import { FaPlus } from "react-icons/fa6"; import { TbChecklist } from "react-icons/tb"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; -export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId }) { +export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId, updateTask }) { const [isChallengeChecked, setChallengeChecked] = useState(challenge); const [isImportantChecked, setImportantChecked] = useState(importance); const [currentDifficulty, setCurrentDifficulty] = useState(difficulty); - // console.log(currentDifficulty); - // console.log(isChallengeChecked); - // console.log(isImportantChecked); + const [selectedTags, setSelectedTags] = useState([]); + const [dateStart, setDateStart] = useState(new Date()); + const [dateEnd, setDateEnd] = useState(new Date()); + const [startDateEnabled, setStartDateEnabled] = useState(false); + const [endDateEnabled, setEndDateEnabled] = useState(false); + const [isTaskComplete, setTaskComplete] = useState(false); const handleChallengeChange = () => { setChallengeChecked(!isChallengeChecked); @@ -23,6 +28,51 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng setCurrentDifficulty(parseInt(event.target.value, 10)); }; + const handleTagChange = (tag) => { + const isSelected = selectedTags.includes(tag); + setSelectedTags(isSelected ? selectedTags.filter((selectedTag) => selectedTag !== tag) : [...selectedTags, tag]); + }; + + const handleStartDateChange = () => { + if (!isTaskComplete) { + setStartDateEnabled(!startDateEnabled); + } + }; + + const handleEndDateChange = () => { + if (!isTaskComplete) { + setEndDateEnabled(!endDateEnabled); + } + }; + + const handleTaskCompleteChange = () => { + if (isTaskComplete) { + setTaskComplete(false); + } else { + setTaskComplete(true); + setStartDateEnabled(false); + setEndDateEnabled(false); + } + }; + + // Existing tags + const existingTags = tags.map((tag, index) => ( +
+ {tag.label} +
+ )); + + // Selected tags + const selectedTagElements = selectedTags.map((tag, index) => ( +
+ {tag.label} +
+ )); + return (
@@ -38,7 +88,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng

{title}

- {/* Tags */}
@@ -46,19 +95,81 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng -
    -
  • - - - Item 2 - -
  • +
      + {tags.map((tag, index) => ( +
    • + +
    • + ))}
-
+
+ {existingTags} + {selectedTagElements} +
+
+ {/* Date Picker */} +
+ {/* Start */} +
+
+

Start At

+
+ +
+ setDateStart(date)} + disabled={!startDateEnabled} + /> +
+
+
+ {/* Complete? */} +
+
+
+

Complete

+ +
+
+
+
+ {/* End */} +
+

End At

+
+ +
+ setDateEnd(date)} disabled={!endDateEnabled} /> +
+
+
- {/* Description */}

@@ -71,7 +182,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng {description}

- {/* Difficulty, Challenge, and Importance */}
@@ -123,7 +233,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
- {/* Subtask */}

@@ -140,7 +249,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng

-
From fcff22e64dfb0e54a3e4ed729bc04f3b9cc3edc7 Mon Sep 17 00:00:00 2001 From: sosokker Date: Mon, 27 Nov 2023 14:17:37 +0700 Subject: [PATCH 20/45] Add user data retreival viewset --- backend/users/urls.py | 5 +++-- backend/users/views.py | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/backend/users/urls.py b/backend/users/urls.py index 474da31..88a7238 100644 --- a/backend/users/urls.py +++ b/backend/users/urls.py @@ -1,7 +1,8 @@ from django.urls import path -from users.views import CustomUserCreate, CustomUserProfileUpdate +from users.views import CustomUserCreate, CustomUserProfileUpdate, UserDataRetriveViewset urlpatterns = [ path('user/create/', CustomUserCreate.as_view(), name="create_user"), - path('user/update/', CustomUserProfileUpdate.as_view(), name='update_user') + path('user/update/', CustomUserProfileUpdate.as_view(), name='update_user'), + path('user/data/', UserDataRetriveViewset.as_view({'get': 'retrieve'}), name="get_user_data"), ] \ No newline at end of file diff --git a/backend/users/views.py b/backend/users/views.py index 9d022e3..7446d7b 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -1,6 +1,6 @@ """This module defines API views for user creation""" -from rest_framework import status +from rest_framework import status, viewsets, mixins from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response @@ -61,4 +61,15 @@ class CustomUserProfileUpdate(APIView): if serializer.is_valid(): serializer.save() return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class UserDataRetriveViewset(viewsets.GenericViewSet, mixins.RetrieveModelMixin): + queryset = CustomUser.objects.all() + permission_classes = (IsAuthenticated,) + serializer_class = UpdateProfileSerializer + + def retrieve(self, request, *args, **kwargs): + serializer = self.get_serializer(request.user) + return Response(serializer.data) + \ No newline at end of file From 00c68538fc3bcf9edd8da425d3c6085aaa594343 Mon Sep 17 00:00:00 2001 From: Pattadon Date: Mon, 27 Nov 2023 14:24:59 +0700 Subject: [PATCH 21/45] Add react-ios-time-picker package and fix merge conflict in PieChart component --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 33 ++++++- .../src/components/dashboard/PieChart.jsx | 4 - .../src/components/kanbanBoard/taskCard.jsx | 20 +---- .../kanbanBoard/taskDetailModal.jsx | 87 +++++++++++++++---- 5 files changed, 102 insertions(+), 43 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 44e4b66..b06025e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -46,6 +46,7 @@ "react-datetime-picker": "^5.5.3", "react-dom": "^18.2.0", "react-icons": "^4.11.0", + "react-ios-time-picker": "^0.2.2", "react-router-dom": "^6.18.0", "react-tsparticles": "^2.12.2", "tsparticles": "^2.12.0" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 2bb6ef0..aca0306 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - dependencies: '@dnd-kit/core': specifier: ^6.1.0 @@ -113,6 +109,9 @@ dependencies: react-icons: specifier: ^4.11.0 version: 4.12.0(react@18.2.0) + react-ios-time-picker: + specifier: ^0.2.2 + version: 0.2.2(react-dom@18.2.0)(react@18.2.0) react-router-dom: specifier: ^6.18.0 version: 6.19.0(react-dom@18.2.0)(react@18.2.0) @@ -3573,6 +3572,17 @@ packages: react: 18.2.0 dev: false + /react-ios-time-picker@0.2.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-bi+K23lK6Pf2xDXmhAlz+RJuy9/onWYi7Ye+ODVhIkis9AVFECOza2ckkZl/4vUypj2+TdTsHn+VZrTNdGIwDQ==} + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-portal: 4.2.2(react-dom@18.2.0)(react@18.2.0) + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -3612,6 +3622,17 @@ packages: warning: 4.0.3 dev: false + /react-portal@4.2.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-vS18idTmevQxyQpnde0Td6ZcUlv+pD8GTyR42n3CHUQq9OHi1C4jDE4ZWEbEsrbrLRhSECYiao58cvocwMtP7Q==} + peerDependencies: + react: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + react-dom: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} peerDependencies: @@ -4663,3 +4684,7 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/frontend/src/components/dashboard/PieChart.jsx b/frontend/src/components/dashboard/PieChart.jsx index 754ac64..c2b94c4 100644 --- a/frontend/src/components/dashboard/PieChart.jsx +++ b/frontend/src/components/dashboard/PieChart.jsx @@ -13,11 +13,7 @@ export function DonutChartGraph() { const completedTask = response.data.total_completed_tasks || 0; const donutData = [ -<<<<<<< HEAD - { name: "Completed task", count: completedTask }, -======= { name: "Completed task", count: completedTask}, ->>>>>>> 4a3f253e3049f97ef4479dd423642897a56e13fc { name: "Total task", count: totalTask }, ]; diff --git a/frontend/src/components/kanbanBoard/taskCard.jsx b/frontend/src/components/kanbanBoard/taskCard.jsx index 13817a8..179ea49 100644 --- a/frontend/src/components/kanbanBoard/taskCard.jsx +++ b/frontend/src/components/kanbanBoard/taskCard.jsx @@ -60,7 +60,7 @@ export function TaskCard({ task, deleteTask, updateTask }) { // Due Date const dueDateTag = task.end_event && new Date(task.end_event) > new Date() - ? (() => { + ? (() => { const daysUntilDue = Math.ceil((new Date(task.end_event) - new Date()) / (1000 * 60 * 60 * 24)); let colorClass = @@ -139,23 +139,6 @@ export function TaskCard({ task, deleteTask, updateTask }) { onMouseLeave={() => { setMouseIsOver(false); }}> -<<<<<<< HEAD -

document.getElementById(`task_detail_modal_${task.id}`).showModal()}> - {task.content} -

- - {mouseIsOver && ( - - )} -======= {/* -------- Task Content -------- */} {/* Tags */} {tags} @@ -183,7 +166,6 @@ export function TaskCard({ task, deleteTask, updateTask }) { {dueDateTag} {subtaskCountTag}
->>>>>>> 4a3f253e3049f97ef4479dd423642897a56e13fc
); diff --git a/frontend/src/components/kanbanBoard/taskDetailModal.jsx b/frontend/src/components/kanbanBoard/taskDetailModal.jsx index e4317e5..707572d 100644 --- a/frontend/src/components/kanbanBoard/taskDetailModal.jsx +++ b/frontend/src/components/kanbanBoard/taskDetailModal.jsx @@ -3,9 +3,21 @@ import { FaTasks, FaRegListAlt } from "react-icons/fa"; import { FaPlus } from "react-icons/fa6"; import { TbChecklist } from "react-icons/tb"; import DatePicker from "react-datepicker"; +import { TimePicker } from "react-ios-time-picker"; import "react-datepicker/dist/react-datepicker.css"; +import { borderColor } from "@mui/system"; -export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId, updateTask }) { +export function TaskDetailModal({ + title, + description, + tags, + difficulty, + challenge, + importance, + taskId, + updateTask, +}) { + let date = new Date(); const [isChallengeChecked, setChallengeChecked] = useState(challenge); const [isImportantChecked, setImportantChecked] = useState(importance); const [currentDifficulty, setCurrentDifficulty] = useState(difficulty); @@ -15,7 +27,11 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng const [startDateEnabled, setStartDateEnabled] = useState(false); const [endDateEnabled, setEndDateEnabled] = useState(false); const [isTaskComplete, setTaskComplete] = useState(false); + const [value, setValue] = useState('10:00'); + const onChange = (timeValue) => { + setValue(timeValue); + }; const handleChallengeChange = () => { setChallengeChecked(!isChallengeChecked); }; @@ -30,7 +46,11 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng const handleTagChange = (tag) => { const isSelected = selectedTags.includes(tag); - setSelectedTags(isSelected ? selectedTags.filter((selectedTag) => selectedTag !== tag) : [...selectedTags, tag]); + setSelectedTags( + isSelected + ? selectedTags.filter((selectedTag) => selectedTag !== tag) + : [...selectedTags, tag] + ); }; const handleStartDateChange = () => { @@ -59,7 +79,8 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng const existingTags = tags.map((tag, index) => (
+ className={`text-xs inline-flex items-center font-bold leading-sm uppercase px-2 py-1 bg-${tag.color}-200 text-${tag.color}-700 rounded-full`} + > {tag.label}
)); @@ -68,7 +89,8 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng const selectedTagElements = selectedTags.map((tag, index) => (
+ className={`text-xs inline-flex items-center font-bold leading-sm uppercase px-2 py-1 bg-${tag.color}-200 text-${tag.color}-700 rounded-full`} + > {tag.label}
)); @@ -92,10 +114,16 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
-
+ +
+ +
{/* Complete? */}
@@ -147,7 +188,7 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
@@ -161,11 +202,19 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng -
- setDateEnd(date)} disabled={!endDateEnabled} /> +
+ setDateEnd(date)} + disabled={!endDateEnabled} + />
@@ -211,7 +260,7 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng @@ -226,7 +275,7 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng @@ -242,7 +291,11 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
- +
- +
From 0c60f48d1845dad342d9717f87543dccdf00ca27 Mon Sep 17 00:00:00 2001 From: Pattadon Date: Mon, 27 Nov 2023 15:37:39 +0700 Subject: [PATCH 22/45] Refactor navigation links and update profile component --- .../src/components/navigations/Navbar.jsx | 4 +- .../profile/ProfileUpdateComponent.jsx | 50 +++++++--- .../src/components/profile/profilePage.jsx | 92 ++++++++++++++----- 3 files changed, 108 insertions(+), 38 deletions(-) diff --git a/frontend/src/components/navigations/Navbar.jsx b/frontend/src/components/navigations/Navbar.jsx index 22120b3..a31def6 100644 --- a/frontend/src/components/navigations/Navbar.jsx +++ b/frontend/src/components/navigations/Navbar.jsx @@ -39,12 +39,12 @@ export function NavBar() { tabIndex={0} className="mt-3 z-[10] p-2 shadow menu menu-sm dropdown-content bg-base-100 rounded-box w-52">
  • - + Navigate(settings.Profile)} className="justify-between"> Profile
  • - Settings + Navigate(settings.Account)}>Settings
  • Logout diff --git a/frontend/src/components/profile/ProfileUpdateComponent.jsx b/frontend/src/components/profile/ProfileUpdateComponent.jsx index 12f5e98..a38558c 100644 --- a/frontend/src/components/profile/ProfileUpdateComponent.jsx +++ b/frontend/src/components/profile/ProfileUpdateComponent.jsx @@ -1,13 +1,30 @@ import { useState, useRef } from "react"; import { ApiUpdateUserProfile } from "src/api/UserProfileApi"; +import { axiosInstance } from "src/api/AxiosConfig"; +import { useEffect } from "react"; export function ProfileUpdateComponent() { const [file, setFile] = useState(null); - const [username, setUsername] = useState(""); - const [fullName, setFullName] = useState(""); - const [about, setAbout] = useState(""); - const defaultImage = "https://i1.sndcdn.com/artworks-cTz48e4f1lxn5Ozp-L3hopw-t500x500.jpg"; + const [firstName, setFirstName] = useState(""); + const [about, setAbout] = useState(); const fileInputRef = useRef(null); + const [profile_pic, setProfilePic] = useState(undefined); + useEffect(() => { + const fetchUser = async () => { + try { + const response = await axiosInstance.get("/user/data/"); + const fetchedProfilePic = response.data.profile_pic; + const fetchedName = response.data.first_name; + const fetchedAbout = response.data.about; + setProfilePic(fetchedProfilePic); + setAbout(fetchedAbout); + setFirstName(fetchedName); + } catch (error) { + console.error("Error fetching user:", error); + } + }; + fetchUser(); + }, []); const handleImageUpload = () => { if (fileInputRef.current) { @@ -25,7 +42,7 @@ export function ProfileUpdateComponent() { const handleSave = () => { const formData = new FormData(); formData.append("profile_pic", file); - formData.append("first_name", username); + formData.append("first_name", firstName); formData.append("about", about); ApiUpdateUserProfile(formData); @@ -45,12 +62,19 @@ export function ProfileUpdateComponent() { ref={fileInputRef} /> -
    +
    {file ? ( - Profile + Profile ) : ( <> - Default + Default @@ -58,7 +82,7 @@ export function ProfileUpdateComponent() {
    - {/* Username Field */} + {/* Username Field
    setUsername(e.target.value)} /> -
    + */} {/* Full Name Field */}
    - + setFullName(e.target.value)} />
    diff --git a/frontend/src/components/profile/profilePage.jsx b/frontend/src/components/profile/profilePage.jsx index d61368a..31284e3 100644 --- a/frontend/src/components/profile/profilePage.jsx +++ b/frontend/src/components/profile/profilePage.jsx @@ -1,36 +1,63 @@ import { ProfileUpdateComponent } from "./ProfileUpdateComponent"; +import { axiosInstance } from "src/api/AxiosConfig"; +import { useEffect, useState } from "react"; export function ProfileUpdatePage() { + const [profile_pic, setProfilePic] = useState(undefined); + const [about, setAbout] = useState(); + useEffect(() => { + const fetchUser = async () => { + try { + const response = await axiosInstance.get("/user/data/"); + const fetchedProfilePic = response.data.profile_pic; + const fetchedAbout = response.data.about; + setProfilePic(fetchedProfilePic); + setAbout(fetchedAbout); + } catch (error) { + console.error("Error fetching user:", error); + } + }; + fetchUser(); + }, []); return (
    -
    Username
    +
    Firstname
    Sirin
    -
    User ID
    + {/*
    User ID
    */}
    - + Profile Picture
    -
    + {/*
    Health
    234/3213
    - +
    32% Remain
    - -
    - + +
    */} +{/*
    Level
    @@ -40,13 +67,18 @@ export function ProfileUpdatePage() { xmlns="http://www.w3.org/2000/svg" fill="#3abff8" viewBox="0 0 24 24" - className="inline-block w-8 h-8"> + className="inline-block w-8 h-8" + >
    3213/321312321 points
    - +
    @@ -58,34 +90,43 @@ export function ProfileUpdatePage() { xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" - className="inline-block w-8 h-8 stroke-current"> + className="inline-block w-8 h-8 stroke-current" + > + d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" + >
    Top 12% of Global Ranking
    - - + + */}

    About me

    - +
    -
    + {/*
    @@ -110,18 +151,21 @@ export function ProfileUpdatePage() {
    -
    +
    */}
    From cbe1e04da0ddef6598f85e920bd47e7aedf22065 Mon Sep 17 00:00:00 2001 From: Chaiyawut Thengket Date: Mon, 27 Nov 2023 15:51:32 +0700 Subject: [PATCH 24/45] add field --- backend/users/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/users/views.py b/backend/users/views.py index 7446d7b..19db6d2 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -72,4 +72,4 @@ class UserDataRetriveViewset(viewsets.GenericViewSet, mixins.RetrieveModelMixin) def retrieve(self, request, *args, **kwargs): serializer = self.get_serializer(request.user) return Response(serializer.data) - \ No newline at end of file + From 3c74d43f4921144e0644d59b16222ec080c7c6ae Mon Sep 17 00:00:00 2001 From: Chaiyawut Thengket Date: Mon, 27 Nov 2023 15:52:13 +0700 Subject: [PATCH 25/45] add field --- backend/users/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/users/views.py b/backend/users/views.py index 19db6d2..9470dc9 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -63,7 +63,7 @@ class CustomUserProfileUpdate(APIView): return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - + class UserDataRetriveViewset(viewsets.GenericViewSet, mixins.RetrieveModelMixin): queryset = CustomUser.objects.all() permission_classes = (IsAuthenticated,) From c98ff37b1b09bd1f8c90332becdd8424cd0ae743 Mon Sep 17 00:00:00 2001 From: Pattadon Date: Mon, 27 Nov 2023 15:56:44 +0700 Subject: [PATCH 26/45] Fix progress calculation and add totalTaskToday state --- .../src/components/dashboard/dashboard.jsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/dashboard/dashboard.jsx b/frontend/src/components/dashboard/dashboard.jsx index 005f98a..74b4ac8 100644 --- a/frontend/src/components/dashboard/dashboard.jsx +++ b/frontend/src/components/dashboard/dashboard.jsx @@ -28,6 +28,7 @@ export function Dashboard() { const [totalTask, setTotalTask] = useState(0); const [totalCompletedTasks, settotalCompletedTasks] = useState(0); const [totalCompletedTasksToday, setTotalCompletedTasksToday] = useState(0); + const [totalTaskToday, setTotalTaskToday] = useState(0); const [progressData, setProgressData] = useState(0); const [overdueTask, setOverdueTask] = useState(0); @@ -36,19 +37,16 @@ export function Dashboard() { const response = await axiosInstance.get("/dashboard/todostats/"); const totalTaskValue = response.data.total_tasks || 0; const totalCompletedTasksValue = response.data.total_completed_tasks || 0; + const totalTaskTodayValue = response.data.total_task_today || 0; const totalCompletedTasksTodayValue = - response.data.total_completed_tasks_today || 0; - const totalTaskToday = response.data.total_task_today || 0; - const totalCompletedTasksToday = response.data.tasks_completed_today || 0; + response.data.tasks_completed_today || 0; const overdueTasks = response.data.overdue_tasks || 0; - - const progress = - (totalCompletedTasksToday / totalCompletedTasksToday) * 100; + const progress = (totalCompletedTasksToday / totalTaskToday) * 100; setTotalTask(totalTaskValue); settotalCompletedTasks(totalCompletedTasksValue); setTotalCompletedTasksToday(totalCompletedTasksTodayValue); - setTotalTaskToday(totalTaskToday); + setTotalTaskToday(totalTaskTodayValue); setProgressData(progress); setOverdueTask(overdueTasks); }; @@ -147,7 +145,11 @@ export function Dashboard() { Date: Mon, 27 Nov 2023 22:14:57 +0700 Subject: [PATCH 27/45] Fix task can't move to other column --- .../kanbanBoard/columnContainer.jsx | 11 +++ .../components/kanbanBoard/kanbanBoard.jsx | 74 +++---------------- 2 files changed, 20 insertions(+), 65 deletions(-) diff --git a/frontend/src/components/kanbanBoard/columnContainer.jsx b/frontend/src/components/kanbanBoard/columnContainer.jsx index d6f4565..6435d31 100644 --- a/frontend/src/components/kanbanBoard/columnContainer.jsx +++ b/frontend/src/components/kanbanBoard/columnContainer.jsx @@ -9,8 +9,17 @@ export function ColumnContainer({ column, createTask, tasks, deleteTask, updateT return tasks.map((task) => task.id); }, [tasks]); + const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({ + id: column.id, + data: { + type: "Column", + column, + }, + }); + return (
    {/* Column title */}
    { - const updatedTasks = tasks.map((task) => - task.id === updatedTask.id ? updatedTask : task - ); + const updatedTasks = tasks.map((task) => (task.id === updatedTask.id ? updatedTask : task)); setTasks(updatedTasks); }; @@ -176,14 +168,8 @@ export function KanbanBoard() { justify-center overflow-x-auto overflow-y-hidden - " - > - + "> +
    {!isLoading ? ( @@ -195,9 +181,7 @@ export function KanbanBoard() { createTask={createTask} deleteTask={deleteTask} updateTask={updateTask} - tasks={(tasks || []).filter( - (task) => task.columnId === col.id - )} + tasks={(tasks || []).filter((task) => task.columnId === col.id)} /> ))}{" "} @@ -210,11 +194,7 @@ export function KanbanBoard() { {createPortal( {/* Render the active task as a draggable overlay */} - + , document.body )} @@ -240,8 +220,6 @@ export function KanbanBoard() { if (!over) return; // If not dropped over anything, exit const activeId = active.id; - const overId = over.id; - const isActiveATask = active.data.current?.type === "Task"; const isOverAColumn = over.data.current?.type === "Column"; @@ -250,8 +228,7 @@ export function KanbanBoard() { setTasks((tasks) => { const activeIndex = tasks.findIndex((t) => t.id === activeId); - // Extract the column ID from overId - const columnId = extractColumnId(overId); + const columnId = over.data.current.column.id; tasks[activeIndex].columnId = columnId; @@ -259,7 +236,7 @@ export function KanbanBoard() { axiosInstance .put(`todo/change_task_list_board/`, { todo_id: activeId, - new_list_board_id: over.data.current.task.columnId, + new_list_board_id: columnId, new_index: 0, }) .then((response) => {}) @@ -271,15 +248,6 @@ export function KanbanBoard() { }); } } - - // Helper function to extract the column ID from the element ID - function extractColumnId(elementId) { - // Implement logic to extract the column ID from elementId - // For example, if elementId is in the format "column-123", you can do: - const parts = elementId.split("-"); - return parts.length === 2 ? parseInt(parts[1], 10) : null; - } - // Handle the drag-over event function onDragOver(event) { const { active, over } = event; @@ -306,39 +274,15 @@ export function KanbanBoard() { tasks[activeIndex].columnId = tasks[overIndex].columnId; return arrayMove(tasks, activeIndex, overIndex - 1); } - axiosInstance - .put(`todo/change_task_list_board/`, { - todo_id: activeId, - new_list_board_id: over.data.current.task.columnId, - new_index: 0, - }) - .then((response) => {}) - .catch((error) => { - console.error("Error updating task columnId:", error); - }); return arrayMove(tasks, activeIndex, overIndex); }); } const isOverAColumn = over.data.current?.type === "Column"; // Move the Task to a different column and update columnId - if ( - isActiveATask && - isOverAColumn && - tasks.some((task) => task.columnId !== overId) - ) { + if (isActiveATask && isOverAColumn && tasks.some((task) => task.columnId !== overId)) { setTasks((tasks) => { const activeIndex = tasks.findIndex((t) => t.id === activeId); - axiosInstance - .put(`todo/change_task_list_board/`, { - todo_id: activeId, - new_list_board_id: over.data.current.task.columnId, - new_index: 0, - }) - .then((response) => {}) - .catch((error) => { - console.error("Error updating task columnId:", error); - }); tasks[activeIndex].columnId = overId; return arrayMove(tasks, activeIndex, activeIndex); }); From 2b013cda45bfcca80e14cc40b64ae5767160614e Mon Sep 17 00:00:00 2001 From: sosokker Date: Mon, 27 Nov 2023 22:37:51 +0700 Subject: [PATCH 28/45] Fix move task over task error --- .../kanbanBoard/columnContainer.jsx | 2 +- .../components/kanbanBoard/kanbanBoard.jsx | 24 +++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/kanbanBoard/columnContainer.jsx b/frontend/src/components/kanbanBoard/columnContainer.jsx index 6435d31..f81b694 100644 --- a/frontend/src/components/kanbanBoard/columnContainer.jsx +++ b/frontend/src/components/kanbanBoard/columnContainer.jsx @@ -9,7 +9,7 @@ export function ColumnContainer({ column, createTask, tasks, deleteTask, updateT return tasks.map((task) => task.id); }, [tasks]); - const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({ + const { setNodeRef, attributes, listeners } = useSortable({ id: column.id, data: { type: "Column", diff --git a/frontend/src/components/kanbanBoard/kanbanBoard.jsx b/frontend/src/components/kanbanBoard/kanbanBoard.jsx index ccfe1b1..0ec4686 100644 --- a/frontend/src/components/kanbanBoard/kanbanBoard.jsx +++ b/frontend/src/components/kanbanBoard/kanbanBoard.jsx @@ -221,17 +221,37 @@ export function KanbanBoard() { const activeId = active.id; const isActiveATask = active.data.current?.type === "Task"; + const isOverATask = over.data.current?.type === "Task"; const isOverAColumn = over.data.current?.type === "Column"; + if (isActiveATask && isOverATask) { + setTasks((tasks) => { + const activeIndex = tasks.findIndex((t) => t.id === activeId); + const columnId = over.data.current.task.columnId; + tasks[activeIndex].columnId = columnId; + // API call to update task's columnId + axiosInstance + .put(`todo/change_task_list_board/`, { + todo_id: activeId, + new_list_board_id: columnId, + new_index: 0, + }) + .then((response) => {}) + .catch((error) => { + console.error("Error updating task columnId:", error); + }); + + return arrayMove(tasks, activeIndex, activeIndex); + }); + } + // Move tasks between columns and update columnId if (isActiveATask && isOverAColumn) { setTasks((tasks) => { const activeIndex = tasks.findIndex((t) => t.id === activeId); - const columnId = over.data.current.column.id; tasks[activeIndex].columnId = columnId; - // API call to update task's columnId axiosInstance .put(`todo/change_task_list_board/`, { From d3a8c90c30a764b89b799153791666678b8a6d6d Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 00:33:39 +0700 Subject: [PATCH 29/45] Add subtask api --- backend/tasks/tasks/serializers.py | 14 ++++++- backend/tasks/tasks/views.py | 60 +++++++++++++++++++++++++++++- backend/tasks/urls.py | 3 +- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/backend/tasks/tasks/serializers.py b/backend/tasks/tasks/serializers.py index 12bea72..65ea0c2 100644 --- a/backend/tasks/tasks/serializers.py +++ b/backend/tasks/tasks/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from users.models import CustomUser from boards.models import ListBoard -from tasks.models import Todo, RecurrenceTask, Habit +from tasks.models import Todo, RecurrenceTask, Habit, Subtask class TaskSerializer(serializers.ModelSerializer): class Meta: @@ -97,4 +97,14 @@ class HabitTaskSerializer(serializers.ModelSerializer): class HabitTaskCreateSerializer(serializers.ModelSerializer): class Meta: model = Habit - exclude = ('tags',) \ No newline at end of file + exclude = ('tags',) + + +class SubTaskSerializer(serializers.ModelSerializer): + class Meta: + model = Subtask + fields = '__all__' + + def create(self, validated_data): + # Create a new task with validated data + return Todo.objects.create(**validated_data) \ No newline at end of file diff --git a/backend/tasks/tasks/views.py b/backend/tasks/tasks/views.py index fe1c82d..18a3410 100644 --- a/backend/tasks/tasks/views.py +++ b/backend/tasks/tasks/views.py @@ -4,10 +4,13 @@ from rest_framework import viewsets, status, serializers from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework import mixins -from .serializers import ChangeTaskListBoardSerializer, ChangeTaskOrderSerializer +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter + +from tasks.tasks.serializers import ChangeTaskListBoardSerializer, ChangeTaskOrderSerializer, SubTaskSerializer from boards.models import ListBoard, KanbanTaskOrder -from tasks.models import Todo, RecurrenceTask, Habit +from tasks.models import Todo, RecurrenceTask, Habit, Subtask from tasks.tasks.serializers import (TaskCreateSerializer, TaskSerializer, RecurrenceTaskSerializer, @@ -117,6 +120,59 @@ class TodoViewSet(viewsets.ModelViewSet): return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) +@extend_schema_view( +list=extend_schema( + parameters=[ + OpenApiParameter(name='parent_task', description='Parent Task ID', type=int), + ] + ) +) +class SubTaskViewset(viewsets.GenericViewSet, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin): + queryset = Subtask.objects.all() + permission_classes = (IsAuthenticated,) + + def get_serializer_class(self): + return SubTaskSerializer + + def list(self, request, *args, **kwargs): + """List only subtask of parent task.""" + try: + parent_task = request.query_params.get('parent_task') + if not parent_task: + raise serializers.ValidationError('parent_task is required.') + queryset = self.get_queryset().filter(parent_task_id=parent_task) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def create(self, request, *args, **kwargs): + """Create a new subtask, point to some parent tasks.""" + try: + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + return Response(serializer.data) + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + def destroy(self, request, *args, **kwargs): + """Delete a subtask.""" + try: + instance = self.get_object() + self.perform_destroy(instance) + return Response(status=status.HTTP_204_NO_CONTENT) + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + class RecurrenceTaskViewSet(viewsets.ModelViewSet): queryset = RecurrenceTask.objects.all() serializer_class = RecurrenceTaskSerializer diff --git a/backend/tasks/urls.py b/backend/tasks/urls.py index d830a65..cf37e23 100644 --- a/backend/tasks/urls.py +++ b/backend/tasks/urls.py @@ -3,7 +3,7 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter from tasks.api import GoogleCalendarEventViewset -from tasks.tasks.views import TodoViewSet, RecurrenceTaskViewSet, HabitTaskViewSet +from tasks.tasks.views import TodoViewSet, RecurrenceTaskViewSet, HabitTaskViewSet, SubTaskViewset from tasks.misc.views import TagViewSet @@ -13,6 +13,7 @@ router.register(r'daily', RecurrenceTaskViewSet) router.register(r'habit', HabitTaskViewSet) router.register(r'tags', TagViewSet) router.register(r'calendar-events', GoogleCalendarEventViewset, basename='calendar-events') +router.register(r'subtasks', SubTaskViewset, basename='subtasks') urlpatterns = [ path('', include(router.urls)), From f9e1250c566cb8b93c993853e86be0313c3c18d0 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 00:43:37 +0700 Subject: [PATCH 30/45] Add subtasks api in react --- frontend/src/api/SubTaskApi.jsx | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 frontend/src/api/SubTaskApi.jsx diff --git a/frontend/src/api/SubTaskApi.jsx b/frontend/src/api/SubTaskApi.jsx new file mode 100644 index 0000000..11a556f --- /dev/null +++ b/frontend/src/api/SubTaskApi.jsx @@ -0,0 +1,33 @@ +import { axiosInstance } from "./AxiosConfig"; + +export const getSubtasks = async (parentTaskId) => { + try { + const response = await axiosInstance.get(`subtasks?parent_task=${parentTaskId}`); + return response.data; + } catch (error) { + console.error("Error fetching subtasks:", error); + throw error; + } +}; + +export const addSubtask = async (parentTaskId, text) => { + try { + const response = await axiosInstance.post("subtasks/", { + text, + parent_task: parentTaskId, + }); + return response.data; + } catch (error) { + console.error("Error adding subtask:", error); + throw error; + } +}; + +export const deleteSubtask = async (subtaskId) => { + try { + await axiosInstance.delete(`subtasks/${subtaskId}/`); + } catch (error) { + console.error("Error deleting subtask:", error); + throw error; + } +}; From cfccebf18903e126590008abe991dd8eeea0d11f Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 01:20:01 +0700 Subject: [PATCH 31/45] Incorrect model in subtask serializer and add PUT PATCH for subtask --- backend/tasks/tasks/serializers.py | 2 +- backend/tasks/tasks/views.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/backend/tasks/tasks/serializers.py b/backend/tasks/tasks/serializers.py index 65ea0c2..d7249c6 100644 --- a/backend/tasks/tasks/serializers.py +++ b/backend/tasks/tasks/serializers.py @@ -107,4 +107,4 @@ class SubTaskSerializer(serializers.ModelSerializer): def create(self, validated_data): # Create a new task with validated data - return Todo.objects.create(**validated_data) \ No newline at end of file + return Subtask.objects.create(**validated_data) \ No newline at end of file diff --git a/backend/tasks/tasks/views.py b/backend/tasks/tasks/views.py index 18a3410..545320a 100644 --- a/backend/tasks/tasks/views.py +++ b/backend/tasks/tasks/views.py @@ -130,7 +130,8 @@ list=extend_schema( class SubTaskViewset(viewsets.GenericViewSet, mixins.CreateModelMixin, mixins.DestroyModelMixin, - mixins.ListModelMixin): + mixins.ListModelMixin, + mixins.UpdateModelMixin): queryset = Subtask.objects.all() permission_classes = (IsAuthenticated,) @@ -173,6 +174,19 @@ class SubTaskViewset(viewsets.GenericViewSet, return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + def partial_update(self, request, *args, **kwargs): + """Update a subtask.""" + try: + instance = self.get_o + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + return Response(serializer.data) + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + class RecurrenceTaskViewSet(viewsets.ModelViewSet): queryset = RecurrenceTask.objects.all() serializer_class = RecurrenceTaskSerializer From 97e4cc5a0dae8e34039a60f766968f37892e5620 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 01:26:40 +0700 Subject: [PATCH 32/45] Fix subtask viewset typo error --- backend/tasks/tasks/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tasks/tasks/views.py b/backend/tasks/tasks/views.py index 545320a..20c4835 100644 --- a/backend/tasks/tasks/views.py +++ b/backend/tasks/tasks/views.py @@ -177,7 +177,7 @@ class SubTaskViewset(viewsets.GenericViewSet, def partial_update(self, request, *args, **kwargs): """Update a subtask.""" try: - instance = self.get_o + instance = self.get_object() serializer = self.get_serializer(instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) self.perform_update(serializer) From 3a7437aa099218b9950ef9bd8a3e322ba783aad9 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 01:28:06 +0700 Subject: [PATCH 33/45] Add subtask CRUD and axios for it --- frontend/src/api/SubTaskApi.jsx | 19 ++- .../kanbanBoard/taskDetailModal.jsx | 145 +++++++++++------- 2 files changed, 103 insertions(+), 61 deletions(-) diff --git a/frontend/src/api/SubTaskApi.jsx b/frontend/src/api/SubTaskApi.jsx index 11a556f..1458153 100644 --- a/frontend/src/api/SubTaskApi.jsx +++ b/frontend/src/api/SubTaskApi.jsx @@ -1,6 +1,6 @@ import { axiosInstance } from "./AxiosConfig"; -export const getSubtasks = async (parentTaskId) => { +export const getSubtask = async (parentTaskId) => { try { const response = await axiosInstance.get(`subtasks?parent_task=${parentTaskId}`); return response.data; @@ -10,10 +10,11 @@ export const getSubtasks = async (parentTaskId) => { } }; -export const addSubtask = async (parentTaskId, text) => { +export const addSubtasks = async (parentTaskId, text) => { try { const response = await axiosInstance.post("subtasks/", { - text, + description: text, + completed: false, parent_task: parentTaskId, }); return response.data; @@ -23,7 +24,7 @@ export const addSubtask = async (parentTaskId, text) => { } }; -export const deleteSubtask = async (subtaskId) => { +export const deleteSubtasks = async (subtaskId) => { try { await axiosInstance.delete(`subtasks/${subtaskId}/`); } catch (error) { @@ -31,3 +32,13 @@ export const deleteSubtask = async (subtaskId) => { throw error; } }; + +export const updateSubtask = async (subtaskId, data) => { + try { + const response = await axiosInstance.patch(`subtasks/${subtaskId}/`, data); + return response.data; + } catch (error) { + console.error("Error updating subtask:", error); + throw error; + } +}; diff --git a/frontend/src/components/kanbanBoard/taskDetailModal.jsx b/frontend/src/components/kanbanBoard/taskDetailModal.jsx index 707572d..ddae1cf 100644 --- a/frontend/src/components/kanbanBoard/taskDetailModal.jsx +++ b/frontend/src/components/kanbanBoard/taskDetailModal.jsx @@ -1,23 +1,13 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { FaTasks, FaRegListAlt } from "react-icons/fa"; -import { FaPlus } from "react-icons/fa6"; +import { FaPlus, FaRegTrashCan } from "react-icons/fa6"; import { TbChecklist } from "react-icons/tb"; import DatePicker from "react-datepicker"; import { TimePicker } from "react-ios-time-picker"; import "react-datepicker/dist/react-datepicker.css"; -import { borderColor } from "@mui/system"; +import { addSubtasks, deleteSubtasks, getSubtask, updateSubtask } from "src/api/SubTaskApi"; -export function TaskDetailModal({ - title, - description, - tags, - difficulty, - challenge, - importance, - taskId, - updateTask, -}) { - let date = new Date(); +export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId, updateTask }) { const [isChallengeChecked, setChallengeChecked] = useState(challenge); const [isImportantChecked, setImportantChecked] = useState(importance); const [currentDifficulty, setCurrentDifficulty] = useState(difficulty); @@ -27,7 +17,9 @@ export function TaskDetailModal({ const [startDateEnabled, setStartDateEnabled] = useState(false); const [endDateEnabled, setEndDateEnabled] = useState(false); const [isTaskComplete, setTaskComplete] = useState(false); - const [value, setValue] = useState('10:00'); + const [value, setValue] = useState("10:00"); + const [subtaskText, setSubtaskText] = useState(""); + const [subtasks, setSubtasks] = useState([]); const onChange = (timeValue) => { setValue(timeValue); @@ -46,11 +38,7 @@ export function TaskDetailModal({ const handleTagChange = (tag) => { const isSelected = selectedTags.includes(tag); - setSelectedTags( - isSelected - ? selectedTags.filter((selectedTag) => selectedTag !== tag) - : [...selectedTags, tag] - ); + setSelectedTags(isSelected ? selectedTags.filter((selectedTag) => selectedTag !== tag) : [...selectedTags, tag]); }; const handleStartDateChange = () => { @@ -75,12 +63,73 @@ export function TaskDetailModal({ } }; + const addSubtask = async () => { + try { + if (subtaskText.trim() !== "") { + const newSubtask = await addSubtasks(taskId, subtaskText.trim()); + setSubtasks([...subtasks, newSubtask]); + setSubtaskText(""); + } + } catch (error) { + console.error("Error adding subtask:", error); + } + }; + + const toggleSubtaskCompletion = async (index) => { + try { + const updatedSubtasks = [...subtasks]; + updatedSubtasks[index].completed = !updatedSubtasks[index].completed; + await updateSubtask(updatedSubtasks[index].id, { completed: updatedSubtasks[index].completed }); + setSubtasks(updatedSubtasks); + } catch (error) { + console.error("Error updating subtask:", error); + } + }; + + const deleteSubtask = async (index) => { + try { + await deleteSubtasks(subtasks[index].id); + const updatedSubtasks = [...subtasks]; + updatedSubtasks.splice(index, 1); + setSubtasks(updatedSubtasks); + } catch (error) { + console.error("Error deleting subtask:", error); + } + }; + + const subtaskElements = subtasks.map((subtask, index) => ( +
    + toggleSubtaskCompletion(index)} + /> +
    + {subtask.description} + deleteSubtask(index)} /> +
    +
    + )); + + useEffect(() => { + const fetchSubtasks = async () => { + try { + const fetchedSubtasks = await getSubtask(taskId); + setSubtasks(fetchedSubtasks); + } catch (error) { + console.error("Error fetching subtasks:", error); + } + }; + + fetchSubtasks(); + }, [taskId]); + // Existing tags const existingTags = tags.map((tag, index) => (
    + className={`text-xs inline-flex items-center font-bold leading-sm uppercase px-2 py-1 bg-${tag.color}-200 text-${tag.color}-700 rounded-full`}> {tag.label}
    )); @@ -89,8 +138,7 @@ export function TaskDetailModal({ const selectedTagElements = selectedTags.map((tag, index) => (
    + className={`text-xs inline-flex items-center font-bold leading-sm uppercase px-2 py-1 bg-${tag.color}-200 text-${tag.color}-700 rounded-full`}> {tag.label}
    )); @@ -114,16 +162,10 @@ export function TaskDetailModal({
    -
    +
    - -
    @@ -202,19 +239,11 @@ export function TaskDetailModal({ -
    - setDateEnd(date)} - disabled={!endDateEnabled} - /> +
    + setDateEnd(date)} disabled={!endDateEnabled} />
    @@ -295,17 +324,19 @@ export function TaskDetailModal({ type="text" placeholder="subtask topic" className="input input-bordered flex-1 w-full" + value={subtaskText} + onChange={(e) => setSubtaskText(e.target.value)} /> -
    + {/* Display Subtasks */} +
    {subtaskElements}
    - +
    From 22e2521413936bb4543b6bca398959a937a10ff3 Mon Sep 17 00:00:00 2001 From: Pattadon Date: Tue, 28 Nov 2023 10:27:14 +0700 Subject: [PATCH 34/45] Add profile picture to navbar --- .../src/components/navigations/Navbar.jsx | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/navigations/Navbar.jsx b/frontend/src/components/navigations/Navbar.jsx index a31def6..dadb503 100644 --- a/frontend/src/components/navigations/Navbar.jsx +++ b/frontend/src/components/navigations/Navbar.jsx @@ -1,6 +1,8 @@ import { useNavigate } from "react-router-dom"; import { apiUserLogout } from "src/api/AuthenticationApi"; import { useAuth } from "src/hooks/AuthHooks"; +import { axiosInstance } from "src/api/AxiosConfig"; +import { useEffect, useState } from "react"; const settings = { Profile: "/profile", @@ -10,6 +12,7 @@ const settings = { export function NavBar() { const Navigate = useNavigate(); const { isAuthenticated, setIsAuthenticated } = useAuth(); + const [profile_pic, setProfilePic] = useState(undefined); const logout = () => { apiUserLogout(); @@ -17,6 +20,25 @@ export function NavBar() { Navigate("/"); }; + useEffect(() => { + const fetchUser = async () => { + if (isAuthenticated) { + try { + const response = await axiosInstance.get("/user/data/"); + const fetchedProfilePic = response.data.profile_pic; + setProfilePic(fetchedProfilePic); + } catch (error) { + console.error("Error fetching user:", error); + } + } else { + setProfilePic( + "https://upload.wikimedia.org/wikipedia/commons/8/89/Portrait_Placeholder.png" + ); + } + }; + fetchUser(); + }, []); + return (
    @@ -32,14 +54,18 @@ export function NavBar() { ) : (
    - -
    From 34cc54f8a7b64eb5d2252fa219b018ef994689ab Mon Sep 17 00:00:00 2001 From: Pattadon Date: Tue, 28 Nov 2023 10:45:57 +0700 Subject: [PATCH 35/45] Update profile page with first name and about me fields --- frontend/src/components/profile/profilePage.jsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/profile/profilePage.jsx b/frontend/src/components/profile/profilePage.jsx index 31284e3..b48912a 100644 --- a/frontend/src/components/profile/profilePage.jsx +++ b/frontend/src/components/profile/profilePage.jsx @@ -5,14 +5,18 @@ import { useEffect, useState } from "react"; export function ProfileUpdatePage() { const [profile_pic, setProfilePic] = useState(undefined); const [about, setAbout] = useState(); + const [firstName, setFirstname] = useState(); useEffect(() => { const fetchUser = async () => { try { const response = await axiosInstance.get("/user/data/"); const fetchedProfilePic = response.data.profile_pic; const fetchedAbout = response.data.about; + console.log(fetchedAbout); + const fetchedFirstname = response.data.first_name; setProfilePic(fetchedProfilePic); setAbout(fetchedAbout); + setFirstname(fetchedFirstname); } catch (error) { console.error("Error fetching user:", error); } @@ -24,7 +28,7 @@ export function ProfileUpdatePage() {
    Firstname
    -
    Sirin
    +
    {firstName}
    {/*
    User ID
    */}
    @@ -57,7 +61,7 @@ export function ProfileUpdatePage() { max="100" >
    */} -{/* + {/*
    Level
    @@ -118,14 +122,11 @@ export function ProfileUpdatePage() { - + placeholder="Enter your about me" + value={about} + >
    - {/*
    From 06b188752f45e51d9047c91e6e2b90e407aa3f14 Mon Sep 17 00:00:00 2001 From: Pattadon Date: Tue, 28 Nov 2023 10:59:05 +0700 Subject: [PATCH 36/45] Update profile page component to display username instead of first name --- frontend/src/components/profile/profilePage.jsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/profile/profilePage.jsx b/frontend/src/components/profile/profilePage.jsx index b48912a..b544323 100644 --- a/frontend/src/components/profile/profilePage.jsx +++ b/frontend/src/components/profile/profilePage.jsx @@ -5,18 +5,17 @@ import { useEffect, useState } from "react"; export function ProfileUpdatePage() { const [profile_pic, setProfilePic] = useState(undefined); const [about, setAbout] = useState(); - const [firstName, setFirstname] = useState(); + const [username, setUsernames] = useState(); useEffect(() => { const fetchUser = async () => { try { const response = await axiosInstance.get("/user/data/"); const fetchedProfilePic = response.data.profile_pic; const fetchedAbout = response.data.about; - console.log(fetchedAbout); - const fetchedFirstname = response.data.first_name; + const fetchedUsernames = response.data.username; setProfilePic(fetchedProfilePic); setAbout(fetchedAbout); - setFirstname(fetchedFirstname); + setUsernames(fetchedUsernames); } catch (error) { console.error("Error fetching user:", error); } @@ -27,8 +26,8 @@ export function ProfileUpdatePage() {
    -
    Firstname
    -
    {firstName}
    +
    Username
    +
    {username}
    {/*
    User ID
    */}
    From 6a583dee903d2ea32d05a986592e1bb8803435d0 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 11:01:10 +0700 Subject: [PATCH 37/45] Give sub_task_count to frontend --- backend/tasks/misc/views.py | 4 +++- backend/tasks/tasks/serializers.py | 9 +++++++++ backend/tasks/tasks/views.py | 12 ++++++++++++ frontend/src/components/kanbanBoard/kanbanBoard.jsx | 1 + 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/backend/tasks/misc/views.py b/backend/tasks/misc/views.py index 3fbc837..8f00572 100644 --- a/backend/tasks/misc/views.py +++ b/backend/tasks/misc/views.py @@ -1,8 +1,10 @@ from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated from ..models import Tag from .serializers import TagSerializer class TagViewSet(viewsets.ModelViewSet): queryset = Tag.objects.all() - serializer_class = TagSerializer \ No newline at end of file + serializer_class = TagSerializer + permission_classes = (IsAuthenticated,) \ No newline at end of file diff --git a/backend/tasks/tasks/serializers.py b/backend/tasks/tasks/serializers.py index d7249c6..9a56632 100644 --- a/backend/tasks/tasks/serializers.py +++ b/backend/tasks/tasks/serializers.py @@ -4,6 +4,9 @@ from boards.models import ListBoard from tasks.models import Todo, RecurrenceTask, Habit, Subtask class TaskSerializer(serializers.ModelSerializer): + tags = serializers.SerializerMethodField() + sub_task_count = serializers.SerializerMethodField() + class Meta: model = Todo fields = '__all__' @@ -19,6 +22,12 @@ class TaskSerializer(serializers.ModelSerializer): validated_data['user'] = user return Todo.objects.create(**validated_data) + def get_tags(self, instance): + return [tag.name for tag in instance.tags.all()] + + def get_sub_task_count(self, instance): + return instance.subtask_set.count() + class TaskCreateSerializer(serializers.ModelSerializer): class Meta: model = Todo diff --git a/backend/tasks/tasks/views.py b/backend/tasks/tasks/views.py index 20c4835..bd5d568 100644 --- a/backend/tasks/tasks/views.py +++ b/backend/tasks/tasks/views.py @@ -35,6 +35,18 @@ class TodoViewSet(viewsets.ModelViewSet): return TaskCreateSerializer return TaskSerializer + def list(self, request, *args, **kwargs): + """ + list all tasks of the authenticated + user and send tags if those Todo too. + """ + try: + queryset = self.get_queryset() + serializer = TaskSerializer(queryset, many=True) + return Response(serializer.data) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + def create(self, request, *args, **kwargs): try: new_task_data = request.data diff --git a/frontend/src/components/kanbanBoard/kanbanBoard.jsx b/frontend/src/components/kanbanBoard/kanbanBoard.jsx index 0ec4686..0a9049d 100644 --- a/frontend/src/components/kanbanBoard/kanbanBoard.jsx +++ b/frontend/src/components/kanbanBoard/kanbanBoard.jsx @@ -121,6 +121,7 @@ export function KanbanBoard() { user: task.user, list_board: task.list_board, tags: task.tags, + subtaskCount: task.sub_task_count, })); setTasks(transformedTasks); From 1876569ec10ef89dd1dd5b6ab3dea1ca45b24a36 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 11:26:10 +0700 Subject: [PATCH 38/45] Fix error when not upload image / Use username instead of first name in profile --- backend/users/serializers.py | 24 +++++++++++++++-- backend/users/views.py | 6 ++++- .../profile/ProfileUpdateComponent.jsx | 27 +++++++------------ 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/backend/users/serializers.py b/backend/users/serializers.py index 962d789..f7ed6fa 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -32,12 +32,32 @@ class UpdateProfileSerializer(serializers.ModelSerializer): Serializer for updating user profile. """ profile_pic = serializers.ImageField(required=False) - first_name = serializers.CharField(max_length=255, required=False) + username = serializers.CharField(max_length=255, required=False) about = serializers.CharField(required=False) class Meta: model = CustomUser - fields = ('profile_pic', 'first_name', 'about') + fields = ('profile_pic', 'username', 'about') + + def update(self, instance, validated_data): + """ + Update an existing user's profile. + """ + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + return instance + +class UpdateProfileNopicSerializer(serializers.ModelSerializer): + """ + Serializer for updating user profile. + """ + username = serializers.CharField(max_length=255, required=False) + about = serializers.CharField(required=False) + + class Meta: + model = CustomUser + fields = ('username', 'about') def update(self, instance, validated_data): """ diff --git a/backend/users/views.py b/backend/users/views.py index 9470dc9..0157c0c 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -9,7 +9,7 @@ from rest_framework.parsers import MultiPartParser from rest_framework_simplejwt.tokens import RefreshToken -from users.serializers import CustomUserSerializer, UpdateProfileSerializer +from users.serializers import CustomUserSerializer, UpdateProfileSerializer, UpdateProfileNopicSerializer from users.models import CustomUser class CustomUserCreate(APIView): @@ -57,7 +57,11 @@ class CustomUserProfileUpdate(APIView): return Response ({ 'error': 'User does not exist' }, status=status.HTTP_404_NOT_FOUND) + serializer = UpdateProfileSerializer(request.user, data=request.data) + if request.data.get('profile_pic') == "null": + serializer = UpdateProfileNopicSerializer(request.user, data=request.data) + if serializer.is_valid(): serializer.save() return Response(serializer.data) diff --git a/frontend/src/components/profile/ProfileUpdateComponent.jsx b/frontend/src/components/profile/ProfileUpdateComponent.jsx index ce3758c..6a918af 100644 --- a/frontend/src/components/profile/ProfileUpdateComponent.jsx +++ b/frontend/src/components/profile/ProfileUpdateComponent.jsx @@ -5,7 +5,7 @@ import { useEffect } from "react"; export function ProfileUpdateComponent() { const [file, setFile] = useState(null); - const [firstName, setFirstName] = useState(""); + const [username, setUserName] = useState(""); const [about, setAbout] = useState(); const fileInputRef = useRef(null); const [profile_pic, setProfilePic] = useState(undefined); @@ -14,11 +14,11 @@ export function ProfileUpdateComponent() { try { const response = await axiosInstance.get("/user/data/"); const fetchedProfilePic = response.data.profile_pic; - const fetchedName = response.data.first_name; + const fetchedName = response.data.username; const fetchedAbout = response.data.about; setProfilePic(fetchedProfilePic); setAbout(fetchedAbout); - setFirstName(fetchedName); + setUserName(fetchedName); } catch (error) { console.error("Error fetching user:", error); } @@ -42,7 +42,7 @@ export function ProfileUpdateComponent() { const handleSave = () => { const formData = new FormData(); formData.append("profile_pic", file); - formData.append("first_name", firstName); + formData.append("username", username); formData.append("about", about); ApiUpdateUserProfile(formData); @@ -62,16 +62,9 @@ export function ProfileUpdateComponent() { ref={fileInputRef} /> -
    +
    {file ? ( - Profile + Profile ) : ( <> Default @@ -96,13 +89,13 @@ export function ProfileUpdateComponent() { {/* Full Name Field */}
    - + setFirstName(e.target.value)} + value={username} + onChange={(e) => setUserName(e.target.value)} />
    From 275738d902583803fd8614a8fbd0a5a55f2ce667 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 11:27:06 +0700 Subject: [PATCH 39/45] Remove console.log from userAPI --- frontend/src/api/UserProfileApi.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/api/UserProfileApi.jsx b/frontend/src/api/UserProfileApi.jsx index 6dfc050..1386e33 100644 --- a/frontend/src/api/UserProfileApi.jsx +++ b/frontend/src/api/UserProfileApi.jsx @@ -11,8 +11,6 @@ const ApiUpdateUserProfile = async (formData) => { }, }); - console.log(response.data); - return response.data; } catch (error) { console.error("Error updating user profile:", error); From 5704d7c9171f3fe2ec034804601033688008a7ef Mon Sep 17 00:00:00 2001 From: Pattadon Date: Tue, 28 Nov 2023 11:45:23 +0700 Subject: [PATCH 40/45] Refactor EisenhowerMatrix component --- .../EisenhowerMatrix/Eisenhower.jsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx b/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx index cc19aaa..9954950 100644 --- a/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx +++ b/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx @@ -1,5 +1,10 @@ import { useState, useEffect } from "react"; -import { FiAlertCircle, FiClock, FiXCircle, FiCheckCircle } from "react-icons/fi"; +import { + FiAlertCircle, + FiClock, + FiXCircle, + FiCheckCircle, +} from "react-icons/fi"; import { readTodoTasks } from "../../api/TaskApi"; import { axiosInstance } from "src/api/AxiosConfig"; @@ -26,7 +31,9 @@ function EachBlog({ name, colorCode, contentList, icon }) { }; return ( -
    +
    {icon} {name} @@ -39,10 +46,14 @@ function EachBlog({ name, colorCode, contentList, icon }) { handleCheckboxChange(index)} /> -
    From c2824ec383897c97701b2cc1171267ac8f53d112 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 11:52:03 +0700 Subject: [PATCH 41/45] Calendar can now only view and delete task --- frontend/src/components/calendar/calendar.jsx | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/calendar/calendar.jsx b/frontend/src/components/calendar/calendar.jsx index 79dae97..c03637a 100644 --- a/frontend/src/components/calendar/calendar.jsx +++ b/frontend/src/components/calendar/calendar.jsx @@ -5,6 +5,7 @@ import dayGridPlugin from "@fullcalendar/daygrid"; import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; import { getEvents, createEventId } from "./TaskDataHandler"; +import { axiosInstance } from "src/api/AxiosConfig"; export class Calendar extends React.Component { state = { @@ -25,13 +26,13 @@ export class Calendar extends React.Component { right: "dayGridMonth,timeGridWeek,timeGridDay", }} initialView="dayGridMonth" - editable={true} - selectable={true} + editable={false} + selectable={false} selectMirror={true} dayMaxEvents={true} weekends={this.state.weekendsVisible} initialEvents={getEvents} - select={this.handleDateSelect} + // select={this.handleDateSelect} eventContent={renderEventContent} eventClick={this.handleEventClick} eventsSet={this.handleEvents} @@ -85,22 +86,22 @@ export class Calendar extends React.Component { }); }; - handleDateSelect = (selectInfo) => { - let title = prompt("Please enter a new title for your event"); - let calendarApi = selectInfo.view.calendar; + // handleDateSelect = (selectInfo) => { + // let title = prompt("Please enter a new title for your event"); + // let calendarApi = selectInfo.view.calendar; - calendarApi.unselect(); // clear date selection + // calendarApi.unselect(); // clear date selection - if (title) { - calendarApi.addEvent({ - id: createEventId(), - title, - start: selectInfo.startStr, - end: selectInfo.endStr, - allDay: selectInfo.allDay, - }); - } - }; + // if (title) { + // calendarApi.addEvent({ + // id: createEventId(), + // title, + // start: selectInfo.startStr, + // end: selectInfo.endStr, + // allDay: selectInfo.allDay, + // }); + // } + // }; handleEventClick = (clickInfo) => { if (confirm(`Are you sure you want to delete the event '${clickInfo.event.title}'`)) { From b7bb1017593e3e70d2639f1be52dd8198e0ac442 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 11:55:39 +0700 Subject: [PATCH 42/45] When move task to Todo, make it completed --- backend/tasks/models.py | 5 ++++ backend/tasks/tests/test_todo_signal.py | 33 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 backend/tasks/tests/test_todo_signal.py diff --git a/backend/tasks/models.py b/backend/tasks/models.py index c848e00..97a2c4b 100644 --- a/backend/tasks/models.py +++ b/backend/tasks/models.py @@ -81,6 +81,11 @@ class Todo(Task): priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT) def save(self, *args, **kwargs): + done_list_name = "Done" + if self.list_board.name == done_list_name: + self.completed = True + Todo.objects.filter(list_board=self.list_board).update(completed=True) + if self.completed and not self.completion_date: self.completion_date = timezone.now() elif not self.completed: diff --git a/backend/tasks/tests/test_todo_signal.py b/backend/tasks/tests/test_todo_signal.py new file mode 100644 index 0000000..fa7fba5 --- /dev/null +++ b/backend/tasks/tests/test_todo_signal.py @@ -0,0 +1,33 @@ +from datetime import datetime, timedelta +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase +from tasks.tests.utils import create_test_user +from tasks.models import Todo +from boards.models import ListBoard, Board + + +class TodoSignalHandlersTests(APITestCase): + def setUp(self): + self.user = create_test_user() + self.client.force_authenticate(user=self.user) + self.list_board = Board.objects.get(user=self.user).listboard_set.first() + + def test_update_priority_signal_handler(self): + """ + Test the behavior of the update_priority signal handler. + """ + due_date = datetime.now() + timedelta(days=5) + data = { + 'title': 'Test Task', + 'type': 'habit', + 'difficulty': 1, + 'end_event': due_date.strftime('%Y-%m-%dT%H:%M:%S'), + 'list_board': self.list_board.id, + } + response = self.client.post(reverse("todo-list"), data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # Retrieve the created task and check if priority is updated + task = Todo.objects.get(title='Test Task') + self.assertIsNotNone(task.priority) # Check if priority is not None \ No newline at end of file From 9affc1ecba59f4553c6542b1089dd1cd5cf60712 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 11:56:25 +0700 Subject: [PATCH 43/45] Add update partial of task (patch api) --- frontend/src/api/TaskApi.jsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frontend/src/api/TaskApi.jsx b/frontend/src/api/TaskApi.jsx index 098d934..0a56767 100644 --- a/frontend/src/api/TaskApi.jsx +++ b/frontend/src/api/TaskApi.jsx @@ -38,6 +38,15 @@ export const updateTask = (endpoint, id, data) => { }); }; +export const updateTaskPartial = (endpoint, id, data) => { + return axiosInstance + .patch(`${baseURL}${endpoint}/${id}/`, data) + .then((response) => response.data) + .catch((error) => { + throw error; + }); +}; + export const deleteTask = (endpoint, id) => { return axiosInstance .delete(`${baseURL}${endpoint}/${id}/`) @@ -64,6 +73,7 @@ export const readHabitTaskByID = (id) => readTaskByID("habit", id); // Update export const updateTodoTask = (id, data) => updateTask("todo", id, data); +export const updateTodoTaskPartial = (id, data) => updateTaskPartial("todo", id, data); export const updateRecurrenceTask = (id, data) => updateTask("daily", id, data); export const updateHabitTask = (id, data) => updateTask("habit", id, data); From 36958d5256053bc4c47a6e58960ffbbef1d4cf9c Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 11:57:53 +0700 Subject: [PATCH 44/45] taskmodal will now work with some apis (need to work with time and desc) - Editing Title - Editing complete, challenge, difficulty --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 11 +- .../kanbanBoard/taskDetailModal.jsx | 198 ++++++++++++++---- 3 files changed, 167 insertions(+), 43 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index b06025e..618e30c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,6 +34,7 @@ "@wojtekmaj/react-daterange-picker": "^5.4.4", "axios": "^1.6.1", "bootstrap": "^5.3.2", + "date-fns": "^2.30.0", "dotenv": "^16.3.1", "framer-motion": "^10.16.4", "gapi-script": "^1.2.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index aca0306..b514b56 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: '@dnd-kit/core': specifier: ^6.1.0 @@ -73,6 +77,9 @@ dependencies: bootstrap: specifier: ^5.3.2 version: 5.3.2(@popperjs/core@2.11.8) + date-fns: + specifier: ^2.30.0 + version: 2.30.0 dotenv: specifier: ^16.3.1 version: 16.3.1 @@ -4684,7 +4691,3 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/frontend/src/components/kanbanBoard/taskDetailModal.jsx b/frontend/src/components/kanbanBoard/taskDetailModal.jsx index ddae1cf..3941ba1 100644 --- a/frontend/src/components/kanbanBoard/taskDetailModal.jsx +++ b/frontend/src/components/kanbanBoard/taskDetailModal.jsx @@ -1,44 +1,141 @@ import { useState, useEffect } from "react"; import { FaTasks, FaRegListAlt } from "react-icons/fa"; -import { FaPlus, FaRegTrashCan } from "react-icons/fa6"; +import { FaPlus, FaRegTrashCan, FaPencil } from "react-icons/fa6"; import { TbChecklist } from "react-icons/tb"; import DatePicker from "react-datepicker"; -import { TimePicker } from "react-ios-time-picker"; import "react-datepicker/dist/react-datepicker.css"; import { addSubtasks, deleteSubtasks, getSubtask, updateSubtask } from "src/api/SubTaskApi"; +import { updateTodoTaskPartial } from "src/api/TaskApi"; +import format from "date-fns/format"; -export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId, updateTask }) { +export function TaskDetailModal({ + title, + description, + tags, + difficulty, + challenge, + importance, + taskId, + updateTask, + completed, +}) { const [isChallengeChecked, setChallengeChecked] = useState(challenge); const [isImportantChecked, setImportantChecked] = useState(importance); - const [currentDifficulty, setCurrentDifficulty] = useState(difficulty); + const [currentDifficulty, setCurrentDifficulty] = useState((difficulty - 1) * 25); const [selectedTags, setSelectedTags] = useState([]); const [dateStart, setDateStart] = useState(new Date()); const [dateEnd, setDateEnd] = useState(new Date()); const [startDateEnabled, setStartDateEnabled] = useState(false); const [endDateEnabled, setEndDateEnabled] = useState(false); - const [isTaskComplete, setTaskComplete] = useState(false); - const [value, setValue] = useState("10:00"); + const [isTaskComplete, setTaskComplete] = useState(completed); + const [starteventValue, setStartEventValue] = useState("10:00 PM"); + const [endeventValue, setEndEventValue] = useState("11:00 AM"); const [subtaskText, setSubtaskText] = useState(""); const [subtasks, setSubtasks] = useState([]); + const [currentTitle, setTitle] = useState(title); + const [isTitleEditing, setTitleEditing] = useState(false); - const onChange = (timeValue) => { - setValue(timeValue); + const handleTitleChange = async () => { + const data = { + title: currentTitle, + }; + await updateTodoTaskPartial(taskId, data); + setTitleEditing(false); }; - const handleChallengeChange = () => { + + const handleStartEventTimeChange = async (timeValue) => { + const formattedTime = convertToFormattedTime(timeValue); + setStartEventValue(formattedTime); + console.log(formattedTime); + const data = { + startTime: formattedTime, + }; + await updateTodoTaskPartial(taskId, data); + }; + + const handleEndEventTimeChange = async (timeValue) => { + const inputTime = event.target.value; + // Validate the input time format + if (!validateTimeFormat(inputTime)) { + // Display an error message or handle invalid format + console.error("Invalid time format. Please use HH:mm AM/PM"); + return; + } + + const formattedTime = convertToFormattedTime(timeValue); + setEndEventValue(formattedTime); + const data = { + endTime: formattedTime, + }; + await updateTodoTaskPartial(taskId, data); + }; + + const convertToFormattedTime = (timeValue) => { + const formattedTime = format(timeValue, "HH:mm:ss.SSSX", { timeZone: "UTC" }); + return formattedTime; + }; + + const validateTimeFormat = (time) => { + const timeFormatRegex = /^(0[1-9]|1[0-2]):[0-5][0-9] (AM|PM)$/i; + return timeFormatRegex.test(time); + }; + + const handleChallengeChange = async () => { setChallengeChecked(!isChallengeChecked); + const data = { + challenge: !isChallengeChecked, + }; + await updateTodoTaskPartial(taskId, data); }; - const handleImportantChange = () => { + const handleImportantChange = async () => { setImportantChecked(!isImportantChecked); + const data = { + important: !isImportantChecked, + }; + await updateTodoTaskPartial(taskId, data); }; - const handleDifficultyChange = (event) => { + const handleDifficultyChange = async (event) => { setCurrentDifficulty(parseInt(event.target.value, 10)); + let diff = event.target.value / 25 + 1; + const data = { + difficulty: diff, + }; + await updateTodoTaskPartial(taskId, data); }; const handleTagChange = (tag) => { const isSelected = selectedTags.includes(tag); setSelectedTags(isSelected ? selectedTags.filter((selectedTag) => selectedTag !== tag) : [...selectedTags, tag]); + ``; + }; + + const handleStartDateValueChange = (date) => { + if (!isTaskComplete) { + setDateStart(date); + const formattedStartDate = convertToFormattedDate(date); + const data = { + startTime: formattedStartDate, + }; + updateTodoTaskPartial(taskId, data); + } + }; + + const handleEndDateValueChange = (date) => { + if (!isTaskComplete) { + setDateEnd(date); + const formattedEndDate = convertToFormattedDate(date); + const data = { + endTime: formattedEndDate, + }; + updateTodoTaskPartial(taskId, data); + } + }; + + const convertToFormattedDate = (dateValue) => { + const formattedDate = format(dateValue, "yyyy-MM-dd'T'", { timeZone: "UTC" }); + return formattedDate; }; const handleStartDateChange = () => { @@ -53,14 +150,21 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng } }; - const handleTaskCompleteChange = () => { + const handleTaskCompleteChange = async () => { + let completed = false; if (isTaskComplete) { setTaskComplete(false); + completed = false; } else { setTaskComplete(true); + completed = true; setStartDateEnabled(false); setEndDateEnabled(false); } + const data = { + completed: completed, + }; + await updateTodoTaskPartial(taskId, data); }; const addSubtask = async () => { @@ -130,7 +234,7 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
    - {tag.label} + {tag.name}
    )); @@ -139,7 +243,7 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
    - {tag.label} + {tag.name}
    )); @@ -149,13 +253,29 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng {/* Title */}
    -

    - - {} - {title} - -

    -

    {title}

    + {isTitleEditing ? ( +
    + + setTitle(e.target.value)} + /> + +
    + ) : ( +

    + + {} + {currentTitle} + setTitleEditing(true)} /> + +

    + )} +

    {currentTitle}

    {/* Tags */} @@ -175,7 +295,7 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng className="checkbox checkbox-sm" onChange={() => handleTagChange(tag)} /> - {tag.label} + {tag}
  • ))} @@ -201,33 +321,31 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng onChange={handleStartDateChange} />
    - setDateStart(date)} - disabled={!startDateEnabled} - /> +
    -
    - + {/* handleStartEventTimeChange */} +
    + {/* Complete? */}

    Complete

    - + +
    @@ -243,8 +361,10 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng onChange={handleEndDateChange} />
    - setDateEnd(date)} disabled={!endDateEnabled} /> +
    + {/* End event time picker */} +
    this is time picker
    From 145f19f49b583afb900e6eb3d59244b311f837a7 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 28 Nov 2023 11:58:08 +0700 Subject: [PATCH 45/45] Change card Style --- .../src/components/kanbanBoard/taskCard.jsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/kanbanBoard/taskCard.jsx b/frontend/src/components/kanbanBoard/taskCard.jsx index 179ea49..4c40a9e 100644 --- a/frontend/src/components/kanbanBoard/taskCard.jsx +++ b/frontend/src/components/kanbanBoard/taskCard.jsx @@ -25,6 +25,10 @@ export function TaskCard({ task, deleteTask, updateTask }) { // ---- DESC AND TAG ---- */ + if (task.tags === undefined) { + task.tags = []; + } + // Tags const tags = task.tags.length > 0 ? ( @@ -32,8 +36,8 @@ export function TaskCard({ task, deleteTask, updateTask }) { {task.tags.map((tag, index) => (
    - {tag.label} + className={`inline-flex items-center font-bold leading-sm uppercase w-1/3 h-3 p-2 mr-1 bg-${tag.color}-200 text-${tag.color}-700 rounded`}> +

    {tag}

    ))} @@ -42,7 +46,7 @@ export function TaskCard({ task, deleteTask, updateTask }) { // difficulty? const difficultyTag = task.difficulty ? ( difficulty @@ -60,7 +64,7 @@ export function TaskCard({ task, deleteTask, updateTask }) { // Due Date const dueDateTag = task.end_event && new Date(task.end_event) > new Date() - ? (() => { + ? (() => { const daysUntilDue = Math.ceil((new Date(task.end_event) - new Date()) / (1000 * 60 * 60 * 24)); let colorClass = @@ -93,7 +97,7 @@ export function TaskCard({ task, deleteTask, updateTask }) { // Subtask count const subtaskCountTag = task.subtaskCount ? ( - + {task.subtaskCount} ) : null; @@ -124,6 +128,7 @@ export function TaskCard({ task, deleteTask, updateTask }) { challenge={task.challenge} importance={task.importance} updateTask={updateTask} + completed={task.completed} /> {/* -------- Task Card -------- */} @@ -138,7 +143,8 @@ export function TaskCard({ task, deleteTask, updateTask }) { }} onMouseLeave={() => { setMouseIsOver(false); - }}> + }} + onClick={() => document.getElementById(`task_detail_modal_${task.id}`).showModal()}> {/* -------- Task Content -------- */} {/* Tags */} {tags}