mirror of
https://github.com/TurTaskProject/TurTaskWeb.git
synced 2025-12-20 06:24:07 +01:00
Merge pull request #88 from TurTaskProject/main
Release large kanban and profile update bug fixes
This commit is contained in:
commit
52781cfec6
@ -3,7 +3,4 @@ from django.apps import AppConfig
|
|||||||
|
|
||||||
class BoardsConfig(AppConfig):
|
class BoardsConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'boards'
|
name = 'boards'
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
import boards.signals
|
|
||||||
@ -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)
|
|
||||||
@ -88,6 +88,7 @@ SPECTACULAR_SETTINGS = {
|
|||||||
'DESCRIPTION': 'API documentation for TurTask',
|
'DESCRIPTION': 'API documentation for TurTask',
|
||||||
'VERSION': '1.0.0',
|
'VERSION': '1.0.0',
|
||||||
'SERVE_INCLUDE_SCHEMA': False,
|
'SERVE_INCLUDE_SCHEMA': False,
|
||||||
|
'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'],
|
||||||
}
|
}
|
||||||
|
|
||||||
REST_USE_JWT = True
|
REST_USE_JWT = True
|
||||||
|
|||||||
@ -88,6 +88,7 @@ SPECTACULAR_SETTINGS = {
|
|||||||
'DESCRIPTION': 'API documentation for TurTask',
|
'DESCRIPTION': 'API documentation for TurTask',
|
||||||
'VERSION': '1.0.0',
|
'VERSION': '1.0.0',
|
||||||
'SERVE_INCLUDE_SCHEMA': False,
|
'SERVE_INCLUDE_SCHEMA': False,
|
||||||
|
'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'],
|
||||||
}
|
}
|
||||||
|
|
||||||
REST_USE_JWT = True
|
REST_USE_JWT = True
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from rest_framework import serializers
|
# from rest_framework import serializers
|
||||||
from .models import UserStats
|
# from .models import UserStats
|
||||||
|
|
||||||
class UserStatsSerializer(serializers.ModelSerializer):
|
# class UserStatsSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
# class Meta:
|
||||||
model = UserStats
|
# model = UserStats
|
||||||
fields = ['health', 'gold', 'experience', 'strength', 'intelligence', 'endurance', 'perception', 'luck', 'level']
|
# fields = ['health', 'gold', 'experience', 'strength', 'intelligence', 'endurance', 'perception', 'luck', 'level']
|
||||||
@ -1,32 +1,35 @@
|
|||||||
from django.test import TestCase
|
from rest_framework.test import APITestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from tasks.models import Todo
|
from tasks.models import Todo
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from datetime import timedelta
|
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):
|
def setUp(self):
|
||||||
self.user = create_test_user()
|
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(
|
return Todo.objects.create(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
title=title,
|
title=title,
|
||||||
completed=completed,
|
completed=completed,
|
||||||
completion_date=completion_date,
|
completion_date=completion_date,
|
||||||
end_event=end_event
|
end_event=end_event,
|
||||||
|
list_board=self.list_board
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_dashboard_stats_view(self):
|
def test_dashboard_stats_view(self):
|
||||||
# Create tasks for testing
|
# Create tasks for testing
|
||||||
self.create_task('Task 1', completed=True)
|
self._create_task('Task 1', completed=True)
|
||||||
self.create_task('Task 2', end_event=timezone.now() - timedelta(days=8))
|
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 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.status_code, 200)
|
||||||
|
|
||||||
self.assertEqual(response.data['completed_this_week'], 1)
|
self.assertEqual(response.data['completed_this_week'], 1)
|
||||||
@ -35,69 +38,9 @@ class DashboardStatsAndWeeklyViewSetTests(TestCase):
|
|||||||
|
|
||||||
def test_dashboard_weekly_view(self):
|
def test_dashboard_weekly_view(self):
|
||||||
# Create tasks for testing
|
# Create tasks for testing
|
||||||
self.create_task('Task 1', completion_date=timezone.now() - timedelta(days=1))
|
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 2', end_event=timezone.now() - timedelta(days=8))
|
||||||
self.create_task('Task 3', end_event=timezone.now())
|
self._create_task('Task 3', end_event=timezone.now())
|
||||||
|
|
||||||
response = self.client.get(reverse('weekly-list'))
|
response = self.client.get(reverse('weekly-list'))
|
||||||
self.assertEqual(response.status_code, 200)
|
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)
|
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
from .views import DashboardStatsViewSet, DashboardWeeklyViewSet
|
from .views import DashboardStatsTodoViewSet, DashboardWeeklyViewSet
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'dashboard/stats', DashboardStatsViewSet, basename='stats')
|
router.register(r'dashboard/todostats', DashboardStatsTodoViewSet, basename='statstodo')
|
||||||
router.register(r'dashboard/weekly', DashboardWeeklyViewSet, basename='weekly')
|
router.register(r'dashboard/weekly', DashboardWeeklyViewSet, basename='weekly')
|
||||||
|
router.register(r'dashboard/recstats', DashboardStatsTodoViewSet, basename='statsrec')
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', include(router.urls)),
|
path('', include(router.urls)),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -5,10 +5,13 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework import viewsets, mixins
|
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,)
|
permission_classes = (IsAuthenticated,)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -66,6 +69,27 @@ class DashboardStatsViewSet(viewsets.GenericViewSet, mixins.ListModelMixin):
|
|||||||
# Overall completion rate
|
# Overall completion rate
|
||||||
total_tasks = Todo.objects.filter(user=user).count()
|
total_tasks = Todo.objects.filter(user=user).count()
|
||||||
overall_completion_rate = (completed_last_7_days / total_tasks) * 100 if total_tasks > 0 else 0
|
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()
|
||||||
|
|
||||||
|
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=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()
|
||||||
|
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"completed_last_7_days": completed_last_7_days,
|
"completed_last_7_days": completed_last_7_days,
|
||||||
@ -75,6 +99,10 @@ class DashboardStatsViewSet(viewsets.GenericViewSet, mixins.ListModelMixin):
|
|||||||
"completed_this_week": completed_this_week,
|
"completed_this_week": completed_this_week,
|
||||||
"overdue_tasks": overdue_tasks,
|
"overdue_tasks": overdue_tasks,
|
||||||
"overall_completion_rate": overall_completion_rate,
|
"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,
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
@ -145,7 +173,142 @@ class DashboardWeeklyViewSet(viewsets.GenericViewSet, mixins.ListModelMixin):
|
|||||||
|
|
||||||
return Response(weekly_stats, status=status.HTTP_200_OK)
|
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):
|
# class DashboardStatsAPIView(APIView):
|
||||||
# permission_classes = [IsAuthenticated]
|
# permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from ..models import Tag
|
from ..models import Tag
|
||||||
from .serializers import TagSerializer
|
from .serializers import TagSerializer
|
||||||
|
|
||||||
|
|
||||||
class TagViewSet(viewsets.ModelViewSet):
|
class TagViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Tag.objects.all()
|
queryset = Tag.objects.all()
|
||||||
serializer_class = TagSerializer
|
serializer_class = TagSerializer
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
@ -81,6 +81,11 @@ class Todo(Task):
|
|||||||
priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT)
|
priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
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:
|
if self.completed and not self.completion_date:
|
||||||
self.completion_date = timezone.now()
|
self.completion_date = timezone.now()
|
||||||
elif not self.completed:
|
elif not self.completed:
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from rest_framework import serializers
|
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):
|
class GoogleCalendarEventSerializer(serializers.Serializer):
|
||||||
@ -17,18 +18,22 @@ class TodoUpdateSerializer(serializers.ModelSerializer):
|
|||||||
updated = serializers.DateTimeField(source="last_update")
|
updated = serializers.DateTimeField(source="last_update")
|
||||||
start_datetime = serializers.DateTimeField(source="start_event", required=False)
|
start_datetime = serializers.DateTimeField(source="start_event", required=False)
|
||||||
end_datetime = serializers.DateTimeField(source="end_event", required=False)
|
end_datetime = serializers.DateTimeField(source="end_event", required=False)
|
||||||
|
list_board = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Todo
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
self.user = kwargs.pop('user', None)
|
self.user = kwargs.pop('user', None)
|
||||||
super(TodoUpdateSerializer, self).__init__(*args, **kwargs)
|
super(TodoUpdateSerializer, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_list_board(self, obj):
|
||||||
|
return Board.objects.get(user=self.user).listboard_set.first()
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
validated_data['user'] = self.user
|
validated_data['user'] = self.user
|
||||||
|
validated_data['list_board'] = self.get_list_board(self)
|
||||||
task = Todo.objects.create(**validated_data)
|
task = Todo.objects.create(**validated_data)
|
||||||
|
|
||||||
return task
|
return task
|
||||||
|
|||||||
@ -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.dispatch import receiver
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from boards.models import ListBoard, Board
|
|
||||||
from tasks.models import Todo
|
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:
|
elif time_until_due <= urgency_threshold and instance.importance < importance_threshold:
|
||||||
instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT
|
instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT
|
||||||
else:
|
else:
|
||||||
instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT
|
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(),
|
|
||||||
)
|
|
||||||
@ -1,16 +1,33 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from users.models import CustomUser
|
||||||
from boards.models import ListBoard
|
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 TaskSerializer(serializers.ModelSerializer):
|
||||||
|
tags = serializers.SerializerMethodField()
|
||||||
|
sub_task_count = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Todo
|
model = Todo
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
def create(self, validated_data):
|
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)
|
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 TaskCreateSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Todo
|
model = Todo
|
||||||
@ -89,4 +106,14 @@ class HabitTaskSerializer(serializers.ModelSerializer):
|
|||||||
class HabitTaskCreateSerializer(serializers.ModelSerializer):
|
class HabitTaskCreateSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Habit
|
model = Habit
|
||||||
exclude = ('tags',)
|
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 Subtask.objects.create(**validated_data)
|
||||||
@ -4,10 +4,13 @@ from rest_framework import viewsets, status, serializers
|
|||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
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 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,
|
from tasks.tasks.serializers import (TaskCreateSerializer,
|
||||||
TaskSerializer,
|
TaskSerializer,
|
||||||
RecurrenceTaskSerializer,
|
RecurrenceTaskSerializer,
|
||||||
@ -32,6 +35,18 @@ class TodoViewSet(viewsets.ModelViewSet):
|
|||||||
return TaskCreateSerializer
|
return TaskCreateSerializer
|
||||||
return TaskSerializer
|
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):
|
def create(self, request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
new_task_data = request.data
|
new_task_data = request.data
|
||||||
@ -117,6 +132,73 @@ class TodoViewSet(viewsets.ModelViewSet):
|
|||||||
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
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,
|
||||||
|
mixins.UpdateModelMixin):
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def partial_update(self, request, *args, **kwargs):
|
||||||
|
"""Update a subtask."""
|
||||||
|
try:
|
||||||
|
instance = self.get_object()
|
||||||
|
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):
|
class RecurrenceTaskViewSet(viewsets.ModelViewSet):
|
||||||
queryset = RecurrenceTask.objects.all()
|
queryset = RecurrenceTask.objects.all()
|
||||||
serializer_class = RecurrenceTaskSerializer
|
serializer_class = RecurrenceTaskSerializer
|
||||||
|
|||||||
@ -1,18 +1,22 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from django.test import TestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from django.utils import timezone
|
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.serializers import TodoUpdateSerializer
|
||||||
from tasks.models import Todo
|
from tasks.models import Todo
|
||||||
|
from boards.models import Board
|
||||||
|
|
||||||
class TaskUpdateSerializerTest(TestCase):
|
class TaskUpdateSerializerTest(APITestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = create_test_user()
|
self.user = create_test_user()
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
self.current_time = '2020-08-01T00:00:00Z'
|
self.current_time = '2020-08-01T00:00:00Z'
|
||||||
self.end_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):
|
def test_serializer_create(self):
|
||||||
data = {
|
data = {
|
||||||
@ -23,6 +27,7 @@ class TaskUpdateSerializerTest(TestCase):
|
|||||||
'updated': self.end_time,
|
'updated': self.end_time,
|
||||||
'start_datetime' : self.current_time,
|
'start_datetime' : self.current_time,
|
||||||
'end_datetie': self.end_time,
|
'end_datetie': self.end_time,
|
||||||
|
'list_board': self.list_board.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
serializer = TodoUpdateSerializer(data=data, user=self.user)
|
serializer = TodoUpdateSerializer(data=data, user=self.user)
|
||||||
@ -32,7 +37,7 @@ class TaskUpdateSerializerTest(TestCase):
|
|||||||
self.assertIsInstance(task, Todo)
|
self.assertIsInstance(task, Todo)
|
||||||
|
|
||||||
def test_serializer_update(self):
|
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 = {
|
data = {
|
||||||
'id': '32141cwaNcapufh8jq2conw',
|
'id': '32141cwaNcapufh8jq2conw',
|
||||||
@ -42,6 +47,7 @@ class TaskUpdateSerializerTest(TestCase):
|
|||||||
'updated': self.end_time,
|
'updated': self.end_time,
|
||||||
'start_datetime' : self.current_time,
|
'start_datetime' : self.current_time,
|
||||||
'end_datetie': self.end_time,
|
'end_datetie': self.end_time,
|
||||||
|
'list_board': self.list_board.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
serializer = TodoUpdateSerializer(instance=task, data=data)
|
serializer = TodoUpdateSerializer(instance=task, data=data)
|
||||||
|
|||||||
@ -2,72 +2,72 @@ from datetime import datetime, timedelta
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
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 tasks.models import Todo
|
||||||
|
from boards.models import ListBoard, Board
|
||||||
|
|
||||||
|
|
||||||
# class TodoViewSetTests(APITestCase):
|
class TodoViewSetTests(APITestCase):
|
||||||
# def setUp(self):
|
def setUp(self):
|
||||||
# self.user = create_test_user()
|
self.user = create_test_user()
|
||||||
# self.client = login_user(self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
# self.url = reverse("todo-list")
|
self.url = reverse("todo-list")
|
||||||
# self.due_date = datetime.now() + timedelta(days=5)
|
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):
|
def test_create_valid_todo(self):
|
||||||
# """
|
"""
|
||||||
# Test creating a valid task using the API.
|
Test creating a valid task using the API.
|
||||||
# """
|
"""
|
||||||
# data = {
|
data = {
|
||||||
# 'title': 'Test Task',
|
'title': 'Test Task',
|
||||||
# 'type': 'habit',
|
'type': 'habit',
|
||||||
# 'exp': 10,
|
'difficulty': 1,
|
||||||
# 'attribute': 'str',
|
'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'),
|
||||||
# 'priority': 1,
|
'list_board': self.list_board.id,
|
||||||
# 'difficulty': 1,
|
}
|
||||||
# 'user': self.user.id,
|
response = self.client.post(self.url, data, format='json')
|
||||||
# 'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'),
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
# }
|
self.assertEqual(Todo.objects.count(), 1)
|
||||||
# response = self.client.post(self.url, data, format='json')
|
self.assertEqual(Todo.objects.get().title, 'Test Task')
|
||||||
# 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):
|
def test_create_invalid_todo(self):
|
||||||
# """
|
"""
|
||||||
# Test creating an invalid task using the API.
|
Test creating an invalid task using the API.
|
||||||
# """
|
"""
|
||||||
# data = {
|
data = {
|
||||||
# 'type': 'invalid', # Invalid task type
|
'type': 'invalid', # Invalid task type
|
||||||
# }
|
}
|
||||||
# response = self.client.post(self.url, data, format='json')
|
response = self.client.post(self.url, data, format='json')
|
||||||
# self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
# self.assertEqual(Todo.objects.count(), 0) # No task should be created
|
self.assertEqual(Todo.objects.count(), 0) # No task should be created
|
||||||
|
|
||||||
# def test_missing_required_fields(self):
|
def test_missing_required_fields(self):
|
||||||
# """
|
"""
|
||||||
# Test creating a task with missing required fields using the API.
|
Test creating a task with missing required fields using the API.
|
||||||
# """
|
"""
|
||||||
# data = {
|
data = {
|
||||||
# 'title': 'Incomplete Task',
|
'type': 'habit',
|
||||||
# 'type': 'habit',
|
}
|
||||||
# }
|
response = self.client.post(self.url, data, format='json')
|
||||||
# response = self.client.post(self.url, data, format='json')
|
self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
# self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(Todo.objects.count(), 0) # No task should be created
|
||||||
# self.assertEqual(Todo.objects.count(), 0) # No task should be created
|
|
||||||
|
|
||||||
# def test_invalid_user_id(self):
|
def test_invalid_user_id(self):
|
||||||
# """
|
"""
|
||||||
# Test creating a task with an invalid user ID using the API.
|
Test creating a task with an invalid user ID using the API (OK because we retreive)
|
||||||
# """
|
id from request.
|
||||||
# data = {
|
"""
|
||||||
# 'title': 'Test Task',
|
data = {
|
||||||
# 'type': 'habit',
|
'title': 'Test Task',
|
||||||
# 'exp': 10,
|
'type': 'habit',
|
||||||
# 'priority': 1,
|
'exp': 10,
|
||||||
# 'difficulty': 1,
|
'priority': 1,
|
||||||
# 'user': 999, # Invalid user ID
|
'difficulty': 1,
|
||||||
# 'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'),
|
'user': -100, # Invalid user ID
|
||||||
# }
|
'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'),
|
||||||
# response = self.client.post(self.url, data, format='json')
|
'list_board': self.list_board.id,
|
||||||
# self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
}
|
||||||
# self.assertEqual(Todo.objects.count(), 0) # No task should be created
|
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
|
||||||
|
|||||||
@ -1,36 +1,39 @@
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from django.test import TestCase
|
from rest_framework.test import APITestCase
|
||||||
from tasks.models import Todo
|
from tasks.models import Todo
|
||||||
from tasks.tests.utils import create_test_user
|
from tasks.tests.utils import create_test_user
|
||||||
|
from boards.models import Board
|
||||||
|
|
||||||
class TodoPriorityTest(TestCase):
|
class TodoPriorityTest(APITestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = create_test_user()
|
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):
|
def test_priority_calculation(self):
|
||||||
# Important = 2, Till Due = none
|
# 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()
|
todo.save()
|
||||||
# 'Not Important & Not Urgent'
|
# 'Not Important & Not Urgent'
|
||||||
self.assertEqual(todo.priority, Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT)
|
self.assertEqual(todo.priority, Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT)
|
||||||
|
|
||||||
due_date = datetime.now(timezone.utc) + timedelta(days=1)
|
due_date = datetime.now(timezone.utc) + timedelta(days=1)
|
||||||
# Important = 4, Till Due = 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()
|
todo.save()
|
||||||
# 'Important & Urgent'
|
# 'Important & Urgent'
|
||||||
self.assertEqual(todo.priority, Todo.EisenhowerMatrix.IMPORTANT_URGENT)
|
self.assertEqual(todo.priority, Todo.EisenhowerMatrix.IMPORTANT_URGENT)
|
||||||
|
|
||||||
due_date = datetime.now(timezone.utc) + timedelta(days=10)
|
due_date = datetime.now(timezone.utc) + timedelta(days=10)
|
||||||
# Important = 3, Till Due = 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()
|
todo.save()
|
||||||
# 'Important & Not Urgent'
|
# 'Important & Not Urgent'
|
||||||
self.assertEqual(todo.priority, Todo.EisenhowerMatrix.IMPORTANT_NOT_URGENT)
|
self.assertEqual(todo.priority, Todo.EisenhowerMatrix.IMPORTANT_NOT_URGENT)
|
||||||
|
|
||||||
due_date = datetime.now(timezone.utc) + timedelta(days=2)
|
due_date = datetime.now(timezone.utc) + timedelta(days=2)
|
||||||
# Important = 1, Till Due = 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()
|
todo.save()
|
||||||
# 'Not Important & Urgent'
|
# 'Not Important & Urgent'
|
||||||
self.assertEqual(todo.priority, Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT)
|
self.assertEqual(todo.priority, Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT)
|
||||||
|
|||||||
33
backend/tasks/tests/test_todo_signal.py
Normal file
33
backend/tasks/tests/test_todo_signal.py
Normal file
@ -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
|
||||||
@ -1,26 +1,24 @@
|
|||||||
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from users.models import CustomUser
|
from users.models import CustomUser
|
||||||
from ..models import Todo
|
from ..models import Todo
|
||||||
|
|
||||||
|
|
||||||
def create_test_user(email="testusertestuser@example.com", username="testusertestuser",
|
def create_test_user(email="testusertestuser@example.com",
|
||||||
first_name="Test", password="testpassword",):
|
username="testusertestuser",
|
||||||
"""create predifined user for testing"""
|
password="testpassword",) -> CustomUser:
|
||||||
return CustomUser.objects.create_user(
|
"""create predifined user without placeholder task for testing"""
|
||||||
email=email,
|
|
||||||
username=username,
|
|
||||||
first_name=first_name,
|
|
||||||
password=password,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def login_user(user):
|
|
||||||
"""Login a user to API client."""
|
|
||||||
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_authenticate(user=user)
|
response = client.post(reverse('create_user'), {'email': email,
|
||||||
return client
|
'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):
|
def create_task_json(user, **kwargs):
|
||||||
@ -29,10 +27,7 @@ def create_task_json(user, **kwargs):
|
|||||||
"title": "Test Task",
|
"title": "Test Task",
|
||||||
"type": "habit",
|
"type": "habit",
|
||||||
"notes": "This is a test task created via the API.",
|
"notes": "This is a test task created via the API.",
|
||||||
"exp": 10,
|
|
||||||
"priority": 1.5,
|
|
||||||
"difficulty": 1,
|
"difficulty": 1,
|
||||||
"attribute": "str",
|
|
||||||
"challenge": False,
|
"challenge": False,
|
||||||
"fromSystem": False,
|
"fromSystem": False,
|
||||||
"creation_date": None,
|
"creation_date": None,
|
||||||
@ -51,8 +46,6 @@ def create_test_task(user, **kwargs):
|
|||||||
'title': "Test Task",
|
'title': "Test Task",
|
||||||
'task_type': 'habit',
|
'task_type': 'habit',
|
||||||
'notes': "This is a test task created via the API.",
|
'notes': "This is a test task created via the API.",
|
||||||
'exp': 10,
|
|
||||||
'priority': 1.5,
|
|
||||||
'difficulty': 1,
|
'difficulty': 1,
|
||||||
'attribute': 'str',
|
'attribute': 'str',
|
||||||
'challenge': False,
|
'challenge': False,
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from django.urls import path, include
|
|||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
from tasks.api import GoogleCalendarEventViewset
|
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
|
from tasks.misc.views import TagViewSet
|
||||||
|
|
||||||
|
|
||||||
@ -13,6 +13,7 @@ router.register(r'daily', RecurrenceTaskViewSet)
|
|||||||
router.register(r'habit', HabitTaskViewSet)
|
router.register(r'habit', HabitTaskViewSet)
|
||||||
router.register(r'tags', TagViewSet)
|
router.register(r'tags', TagViewSet)
|
||||||
router.register(r'calendar-events', GoogleCalendarEventViewset, basename='calendar-events')
|
router.register(r'calendar-events', GoogleCalendarEventViewset, basename='calendar-events')
|
||||||
|
router.register(r'subtasks', SubTaskViewset, basename='subtasks')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', include(router.urls)),
|
path('', include(router.urls)),
|
||||||
|
|||||||
@ -32,12 +32,32 @@ class UpdateProfileSerializer(serializers.ModelSerializer):
|
|||||||
Serializer for updating user profile.
|
Serializer for updating user profile.
|
||||||
"""
|
"""
|
||||||
profile_pic = serializers.ImageField(required=False)
|
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)
|
about = serializers.CharField(required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomUser
|
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):
|
def update(self, instance, validated_data):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,9 +1,74 @@
|
|||||||
|
from django.utils import timezone
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from tasks.models import Todo
|
||||||
from users.models import CustomUser, UserStats
|
from users.models import CustomUser, UserStats
|
||||||
|
from boards.models import ListBoard, Board
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=CustomUser)
|
@receiver(post_save, sender=CustomUser)
|
||||||
def create_user_stats(sender, instance, created, **kwargs):
|
def create_user_stats(sender, instance, created, **kwargs):
|
||||||
if created:
|
if created:
|
||||||
UserStats.objects.create(user=instance)
|
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(),
|
||||||
|
)
|
||||||
@ -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)
|
||||||
@ -1,7 +1,8 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from users.views import CustomUserCreate, CustomUserProfileUpdate
|
from users.views import CustomUserCreate, CustomUserProfileUpdate, UserDataRetriveViewset
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('user/create/', CustomUserCreate.as_view(), name="create_user"),
|
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"),
|
||||||
]
|
]
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"""This module defines API views for user creation"""
|
"""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.permissions import AllowAny, IsAuthenticated
|
||||||
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -9,7 +9,7 @@ from rest_framework.parsers import MultiPartParser
|
|||||||
|
|
||||||
from rest_framework_simplejwt.tokens import RefreshToken
|
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
|
from users.models import CustomUser
|
||||||
|
|
||||||
class CustomUserCreate(APIView):
|
class CustomUserCreate(APIView):
|
||||||
@ -57,8 +57,23 @@ class CustomUserProfileUpdate(APIView):
|
|||||||
return Response ({
|
return Response ({
|
||||||
'error': 'User does not exist'
|
'error': 'User does not exist'
|
||||||
}, status=status.HTTP_404_NOT_FOUND)
|
}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
serializer = UpdateProfileSerializer(request.user, data=request.data)
|
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():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
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)
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,7 @@
|
|||||||
"@wojtekmaj/react-daterange-picker": "^5.4.4",
|
"@wojtekmaj/react-daterange-picker": "^5.4.4",
|
||||||
"axios": "^1.6.1",
|
"axios": "^1.6.1",
|
||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.2",
|
||||||
|
"date-fns": "^2.30.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"framer-motion": "^10.16.4",
|
"framer-motion": "^10.16.4",
|
||||||
"gapi-script": "^1.2.0",
|
"gapi-script": "^1.2.0",
|
||||||
@ -42,9 +43,11 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-bootstrap": "^2.9.1",
|
"react-bootstrap": "^2.9.1",
|
||||||
|
"react-datepicker": "^4.23.0",
|
||||||
"react-datetime-picker": "^5.5.3",
|
"react-datetime-picker": "^5.5.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^4.11.0",
|
"react-icons": "^4.11.0",
|
||||||
|
"react-ios-time-picker": "^0.2.2",
|
||||||
"react-router-dom": "^6.18.0",
|
"react-router-dom": "^6.18.0",
|
||||||
"react-tsparticles": "^2.12.2",
|
"react-tsparticles": "^2.12.2",
|
||||||
"tsparticles": "^2.12.0"
|
"tsparticles": "^2.12.0"
|
||||||
|
|||||||
@ -77,6 +77,9 @@ dependencies:
|
|||||||
bootstrap:
|
bootstrap:
|
||||||
specifier: ^5.3.2
|
specifier: ^5.3.2
|
||||||
version: 5.3.2(@popperjs/core@2.11.8)
|
version: 5.3.2(@popperjs/core@2.11.8)
|
||||||
|
date-fns:
|
||||||
|
specifier: ^2.30.0
|
||||||
|
version: 2.30.0
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.3.1
|
specifier: ^16.3.1
|
||||||
version: 16.3.1
|
version: 16.3.1
|
||||||
@ -101,6 +104,9 @@ dependencies:
|
|||||||
react-bootstrap:
|
react-bootstrap:
|
||||||
specifier: ^2.9.1
|
specifier: ^2.9.1
|
||||||
version: 2.9.1(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0)
|
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:
|
react-datetime-picker:
|
||||||
specifier: ^5.5.3
|
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)
|
version: 5.5.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -110,6 +116,9 @@ dependencies:
|
|||||||
react-icons:
|
react-icons:
|
||||||
specifier: ^4.11.0
|
specifier: ^4.11.0
|
||||||
version: 4.12.0(react@18.2.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:
|
react-router-dom:
|
||||||
specifier: ^6.18.0
|
specifier: ^6.18.0
|
||||||
version: 6.19.0(react-dom@18.2.0)(react@18.2.0)
|
version: 6.19.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -3473,6 +3482,22 @@ packages:
|
|||||||
- '@types/react-dom'
|
- '@types/react-dom'
|
||||||
dev: false
|
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):
|
/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==}
|
resolution: {integrity: sha512-bWGEPwGrZjaXTB8P4pbTSDygctLaqTWp0nNibaz8po+l4eTh9gv3yiJ+n4NIcpIJDqZaQJO57Bnij2rAFVQyLw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -3520,6 +3545,10 @@ packages:
|
|||||||
scheduler: 0.23.0
|
scheduler: 0.23.0
|
||||||
dev: false
|
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):
|
/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==}
|
resolution: {integrity: sha512-y/TYovCCBzfIwRJsbLj0rH4Es40wPQhU5GPPq9GlbdF09b0OdzTdMSkBza0QixSlgFzTm6dkM7oTFzaVvaBx+w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -3550,6 +3579,17 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
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:
|
/react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
@ -3565,6 +3605,41 @@ packages:
|
|||||||
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
|
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
|
||||||
dev: false
|
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-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):
|
/react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==}
|
resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
44
frontend/src/api/SubTaskApi.jsx
Normal file
44
frontend/src/api/SubTaskApi.jsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { axiosInstance } from "./AxiosConfig";
|
||||||
|
|
||||||
|
export const getSubtask = 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 addSubtasks = async (parentTaskId, text) => {
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.post("subtasks/", {
|
||||||
|
description: text,
|
||||||
|
completed: false,
|
||||||
|
parent_task: parentTaskId,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error adding subtask:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteSubtasks = async (subtaskId) => {
|
||||||
|
try {
|
||||||
|
await axiosInstance.delete(`subtasks/${subtaskId}/`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting subtask:", error);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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) => {
|
export const deleteTask = (endpoint, id) => {
|
||||||
return axiosInstance
|
return axiosInstance
|
||||||
.delete(`${baseURL}${endpoint}/${id}/`)
|
.delete(`${baseURL}${endpoint}/${id}/`)
|
||||||
@ -64,6 +73,7 @@ export const readHabitTaskByID = (id) => readTaskByID("habit", id);
|
|||||||
|
|
||||||
// Update
|
// Update
|
||||||
export const updateTodoTask = (id, data) => updateTask("todo", id, data);
|
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 updateRecurrenceTask = (id, data) => updateTask("daily", id, data);
|
||||||
export const updateHabitTask = (id, data) => updateTask("habit", id, data);
|
export const updateHabitTask = (id, data) => updateTask("habit", id, data);
|
||||||
|
|
||||||
|
|||||||
@ -11,8 +11,6 @@ const ApiUpdateUserProfile = async (formData) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(response.data);
|
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating user profile:", error);
|
console.error("Error updating user profile:", error);
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { useState, useEffect } from "react";
|
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 { readTodoTasks } from "../../api/TaskApi";
|
||||||
import { axiosInstance } from "src/api/AxiosConfig";
|
import { axiosInstance } from "src/api/AxiosConfig";
|
||||||
|
|
||||||
@ -26,7 +31,9 @@ function EachBlog({ name, colorCode, contentList, icon }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`h-full text-left p-4 rounded-lg bg-white border border-gray-300 overflow-y-auto`}>
|
<div
|
||||||
|
className={`h-full text-left p-4 rounded-lg bg-white border border-gray-300 overflow-y-auto`}
|
||||||
|
>
|
||||||
<div className="flex" style={{ color: colorCode }}>
|
<div className="flex" style={{ color: colorCode }}>
|
||||||
<span className="mx-2 mt-1">{icon}</span>
|
<span className="mx-2 mt-1">{icon}</span>
|
||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
@ -39,10 +46,14 @@ function EachBlog({ name, colorCode, contentList, icon }) {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={item.completed}
|
checked={item.completed}
|
||||||
className="checkbox mt-1 mr-2"
|
className="checkbox mt-1 mr-2 bg-gray-300 "
|
||||||
onChange={() => handleCheckboxChange(index)}
|
onChange={() => handleCheckboxChange(index)}
|
||||||
/>
|
/>
|
||||||
<label className={`cursor-pointer ${item.completed ? "line-through text-gray-500" : ""}`}>
|
<label
|
||||||
|
className={`cursor-pointer ${
|
||||||
|
item.completed ? "line-through text-gray-500" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import dayGridPlugin from "@fullcalendar/daygrid";
|
|||||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
import interactionPlugin from "@fullcalendar/interaction";
|
import interactionPlugin from "@fullcalendar/interaction";
|
||||||
import { getEvents, createEventId } from "./TaskDataHandler";
|
import { getEvents, createEventId } from "./TaskDataHandler";
|
||||||
|
import { axiosInstance } from "src/api/AxiosConfig";
|
||||||
|
|
||||||
export class Calendar extends React.Component {
|
export class Calendar extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
@ -25,13 +26,13 @@ export class Calendar extends React.Component {
|
|||||||
right: "dayGridMonth,timeGridWeek,timeGridDay",
|
right: "dayGridMonth,timeGridWeek,timeGridDay",
|
||||||
}}
|
}}
|
||||||
initialView="dayGridMonth"
|
initialView="dayGridMonth"
|
||||||
editable={true}
|
editable={false}
|
||||||
selectable={true}
|
selectable={false}
|
||||||
selectMirror={true}
|
selectMirror={true}
|
||||||
dayMaxEvents={true}
|
dayMaxEvents={true}
|
||||||
weekends={this.state.weekendsVisible}
|
weekends={this.state.weekendsVisible}
|
||||||
initialEvents={getEvents}
|
initialEvents={getEvents}
|
||||||
select={this.handleDateSelect}
|
// select={this.handleDateSelect}
|
||||||
eventContent={renderEventContent}
|
eventContent={renderEventContent}
|
||||||
eventClick={this.handleEventClick}
|
eventClick={this.handleEventClick}
|
||||||
eventsSet={this.handleEvents}
|
eventsSet={this.handleEvents}
|
||||||
@ -85,22 +86,22 @@ export class Calendar extends React.Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDateSelect = (selectInfo) => {
|
// handleDateSelect = (selectInfo) => {
|
||||||
let title = prompt("Please enter a new title for your event");
|
// let title = prompt("Please enter a new title for your event");
|
||||||
let calendarApi = selectInfo.view.calendar;
|
// let calendarApi = selectInfo.view.calendar;
|
||||||
|
|
||||||
calendarApi.unselect(); // clear date selection
|
// calendarApi.unselect(); // clear date selection
|
||||||
|
|
||||||
if (title) {
|
// if (title) {
|
||||||
calendarApi.addEvent({
|
// calendarApi.addEvent({
|
||||||
id: createEventId(),
|
// id: createEventId(),
|
||||||
title,
|
// title,
|
||||||
start: selectInfo.startStr,
|
// start: selectInfo.startStr,
|
||||||
end: selectInfo.endStr,
|
// end: selectInfo.endStr,
|
||||||
allDay: selectInfo.allDay,
|
// allDay: selectInfo.allDay,
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
handleEventClick = (clickInfo) => {
|
handleEventClick = (clickInfo) => {
|
||||||
if (confirm(`Are you sure you want to delete the event '${clickInfo.event.title}'`)) {
|
if (confirm(`Are you sure you want to delete the event '${clickInfo.event.title}'`)) {
|
||||||
|
|||||||
@ -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 { useEffect, useState } from "react";
|
||||||
import { axiosInstance } from "src/api/AxiosConfig";
|
import { axiosInstance } from "src/api/AxiosConfig";
|
||||||
|
|
||||||
@ -13,7 +20,7 @@ export function KpiCard() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchKpiCardData = async () => {
|
const fetchKpiCardData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get("/dashboard/stats/");
|
const response = await axiosInstance.get("/dashboard/todostats/");
|
||||||
const completedThisWeek = response.data.completed_this_week || 0;
|
const completedThisWeek = response.data.completed_this_week || 0;
|
||||||
const completedLastWeek = response.data.completed_last_week || 0;
|
const completedLastWeek = response.data.completed_last_week || 0;
|
||||||
const percentage = (completedThisWeek / completedLastWeek) * 100;
|
const percentage = (completedThisWeek / completedLastWeek) * 100;
|
||||||
@ -46,10 +53,16 @@ export function KpiCard() {
|
|||||||
<div>
|
<div>
|
||||||
<Metric>{kpiCardData.completedThisWeek}</Metric>
|
<Metric>{kpiCardData.completedThisWeek}</Metric>
|
||||||
</div>
|
</div>
|
||||||
<BadgeDelta deltaType={kpiCardData.incOrdec}>{kpiCardData.percentage.toFixed(0)}%</BadgeDelta>
|
<BadgeDelta deltaType={kpiCardData.incOrdec}>
|
||||||
|
{isNaN(kpiCardData.percentage) || !isFinite(kpiCardData.percentage)
|
||||||
|
? "0%"
|
||||||
|
: `${kpiCardData.percentage.toFixed(0)}%`}
|
||||||
|
</BadgeDelta>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex className="mt-4">
|
<Flex className="mt-4">
|
||||||
<Text className="truncate">vs. {kpiCardData.completedLastWeek} (last week)</Text>
|
<Text className="truncate">
|
||||||
|
vs. {kpiCardData.completedLastWeek} (last week)
|
||||||
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<ProgressBar value={kpiCardData.percentage} className="mt-2" />
|
<ProgressBar value={kpiCardData.percentage} className="mt-2" />
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -8,13 +8,13 @@ export function DonutChartGraph() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchDonutData = async () => {
|
const fetchDonutData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get("/dashboard/stats/");
|
const response = await axiosInstance.get("/dashboard/todostats/");
|
||||||
const todoCount = response.data.todo_count || 0;
|
const totalTask = response.data.total_tasks || 0;
|
||||||
const recurrenceCount = response.data.recurrence_count || 0;
|
const completedTask = response.data.total_completed_tasks || 0;
|
||||||
|
|
||||||
const donutData = [
|
const donutData = [
|
||||||
{ name: "Todo", count: todoCount },
|
{ name: "Completed task", count: completedTask},
|
||||||
{ name: "Recurrence", count: recurrenceCount },
|
{ name: "Total task", count: totalTask },
|
||||||
];
|
];
|
||||||
|
|
||||||
setDonutData(donutData);
|
setDonutData(donutData);
|
||||||
@ -31,9 +31,10 @@ export function DonutChartGraph() {
|
|||||||
data={donutData}
|
data={donutData}
|
||||||
category="count"
|
category="count"
|
||||||
index="name"
|
index="name"
|
||||||
colors={["rose", "yellow", "orange"]}
|
colors={["rose", "yellow"]}
|
||||||
showAnimation
|
showAnimation
|
||||||
radius={25}
|
radius={25}
|
||||||
|
variant="pie"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -8,7 +8,7 @@ export function ProgressCircleChart() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchProgressData = async () => {
|
const fetchProgressData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get("/dashboard/stats/");
|
const response = await axiosInstance.get("/dashboard/todostats/");
|
||||||
let completedLastWeek = response.data.completed_last_week || 0;
|
let completedLastWeek = response.data.completed_last_week || 0;
|
||||||
let assignLastWeek = response.data.tasks_assigned_last_week || 0;
|
let assignLastWeek = response.data.tasks_assigned_last_week || 0;
|
||||||
|
|
||||||
@ -33,9 +33,18 @@ export function ProgressCircleChart() {
|
|||||||
return (
|
return (
|
||||||
<Card className="max-w-lg mx-auto">
|
<Card className="max-w-lg mx-auto">
|
||||||
<Flex className="flex-col items-center">
|
<Flex className="flex-col items-center">
|
||||||
<ProgressCircle className="mt-6" value={progressData} size={200} strokeWidth={10} radius={60} color="indigo">
|
<ProgressCircle
|
||||||
|
className="mt-6"
|
||||||
|
value={progressData}
|
||||||
|
size={200}
|
||||||
|
strokeWidth={10}
|
||||||
|
radius={60}
|
||||||
|
color="indigo"
|
||||||
|
>
|
||||||
<span className="h-12 w-12 rounded-full bg-indigo-100 flex items-center justify-center text-sm text-indigo-500 font-medium">
|
<span className="h-12 w-12 rounded-full bg-indigo-100 flex items-center justify-center text-sm text-indigo-500 font-medium">
|
||||||
{progressData.toFixed(0)} %
|
{isNaN(progressData) || !isFinite(progressData)
|
||||||
|
? "0%"
|
||||||
|
: `${progressData.toFixed(0)}%`}
|
||||||
</span>
|
</span>
|
||||||
</ProgressCircle>
|
</ProgressCircle>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@ -1,22 +1,64 @@
|
|||||||
import { Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title, Legend } from "@tremor/react";
|
import {
|
||||||
|
Card,
|
||||||
|
Grid,
|
||||||
|
Tab,
|
||||||
|
TabGroup,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
TabPanels,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Legend,
|
||||||
|
Metric,
|
||||||
|
ProgressCircle,
|
||||||
|
Flex,
|
||||||
|
} from "@tremor/react";
|
||||||
import { KpiCard } from "./KpiCard";
|
import { KpiCard } from "./KpiCard";
|
||||||
import { BarChartGraph } from "./Barchart";
|
import { BarChartGraph } from "./Barchart";
|
||||||
import { DonutChartGraph } from "./DonutChart";
|
|
||||||
import { AreaChartGraph } from "./Areachart";
|
import { AreaChartGraph } from "./Areachart";
|
||||||
|
import { DonutChartGraph } from "./PieChart";
|
||||||
import { ProgressCircleChart } from "./ProgressCircle";
|
import { ProgressCircleChart } from "./ProgressCircle";
|
||||||
import { useState } from "react";
|
import { axiosInstance } from "src/api/AxiosConfig";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const valueFormatter = (number) =>
|
||||||
|
`$ ${new Intl.NumberFormat("us").format(number).toString()}`;
|
||||||
|
|
||||||
export function Dashboard() {
|
export function Dashboard() {
|
||||||
const [value, setValue] = useState({
|
const [totalTask, setTotalTask] = useState(0);
|
||||||
from: new Date(2021, 0, 1),
|
const [totalCompletedTasks, settotalCompletedTasks] = useState(0);
|
||||||
to: new Date(2023, 0, 7),
|
const [totalCompletedTasksToday, setTotalCompletedTasksToday] = useState(0);
|
||||||
});
|
const [totalTaskToday, setTotalTaskToday] = useState(0);
|
||||||
|
const [progressData, setProgressData] = useState(0);
|
||||||
|
const [overdueTask, setOverdueTask] = 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 totalTaskTodayValue = response.data.total_task_today || 0;
|
||||||
|
const totalCompletedTasksTodayValue =
|
||||||
|
response.data.tasks_completed_today || 0;
|
||||||
|
const overdueTasks = response.data.overdue_tasks || 0;
|
||||||
|
const progress = (totalCompletedTasksToday / totalTaskToday) * 100;
|
||||||
|
|
||||||
|
setTotalTask(totalTaskValue);
|
||||||
|
settotalCompletedTasks(totalCompletedTasksValue);
|
||||||
|
setTotalCompletedTasksToday(totalCompletedTasksTodayValue);
|
||||||
|
setTotalTaskToday(totalTaskTodayValue);
|
||||||
|
setProgressData(progress);
|
||||||
|
setOverdueTask(overdueTasks);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col p-12">
|
<div className="flex flex-col p-12">
|
||||||
<div>
|
<div>
|
||||||
<Title>Dashboard</Title>
|
<Title>Dashboard</Title>
|
||||||
<Text>All of your progress will be shown right here.</Text>
|
<Text>All of your progress will be shown right here.</Text>
|
||||||
<br />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -39,8 +81,9 @@ export function Dashboard() {
|
|||||||
<ProgressCircleChart />
|
<ProgressCircleChart />
|
||||||
<Legend
|
<Legend
|
||||||
className="mt-3 mx-auto w-1/2"
|
className="mt-3 mx-auto w-1/2"
|
||||||
categories={["Completed Tasks", "Assigned Tasks"]}
|
categories={["Completed Tasks"]}
|
||||||
colors={["indigo"]}></Legend>
|
colors={["indigo"]}
|
||||||
|
></Legend>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<BarChartGraph />
|
<BarChartGraph />
|
||||||
@ -50,19 +93,83 @@ export function Dashboard() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
{/*Overview Tab*/}
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<div className="h-31">
|
<Grid numItemsMd={2} numItemsLg={3} className="gap-6 mt-6">
|
||||||
|
<Card>
|
||||||
|
<Title className="mx-auto">Overview</Title>
|
||||||
|
<Card
|
||||||
|
className="max-w-xs mx-auto"
|
||||||
|
decoration="top"
|
||||||
|
decorationColor="yellow"
|
||||||
|
>
|
||||||
|
<Text>Total tasks</Text>
|
||||||
|
<Metric>{totalTask}</Metric>
|
||||||
|
</Card>
|
||||||
|
<br></br>
|
||||||
|
<Card
|
||||||
|
className="max-w-xs mx-auto"
|
||||||
|
decoration="top"
|
||||||
|
decorationColor="rose"
|
||||||
|
>
|
||||||
|
<Text>Total completed tasks</Text>
|
||||||
|
<Metric>{totalCompletedTasks}</Metric>
|
||||||
|
</Card>
|
||||||
|
<br></br>
|
||||||
|
<Card
|
||||||
|
className="max-w-xs mx-auto"
|
||||||
|
decoration="top"
|
||||||
|
decorationColor="pink"
|
||||||
|
>
|
||||||
|
<Text>Overdue tasks</Text>
|
||||||
|
<Metric>{overdueTask}</Metric>
|
||||||
|
</Card>
|
||||||
|
<br></br>
|
||||||
|
</Card>
|
||||||
|
{/*Pie chart graph*/}
|
||||||
<Card className="mx-auto h-full">
|
<Card className="mx-auto h-full">
|
||||||
<Title>Tasks</Title>
|
<Title>Overall completion rate</Title>
|
||||||
<DonutChartGraph />
|
<DonutChartGraph />
|
||||||
<br />
|
<br />
|
||||||
<Legend
|
<Legend
|
||||||
className="mt-3 mx-auto w-1/2"
|
className="mt-3 mx-auto w-1/2"
|
||||||
categories={["Todo Task", "Recurrence Task"]}
|
categories={["Completed Task", "Total Task"]}
|
||||||
colors={["rose", "yellow"]}
|
colors={["rose", "yellow"]}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
{/*Progress circle graph*/}
|
||||||
|
|
||||||
|
<Card className="max-w-lg mx-auto">
|
||||||
|
<Title>Today's progress</Title>
|
||||||
|
<br />
|
||||||
|
<Flex className="flex-col items-center">
|
||||||
|
<ProgressCircle
|
||||||
|
className="mt-6"
|
||||||
|
value={
|
||||||
|
isNaN(progressData) || !isFinite(progressData)
|
||||||
|
? 0
|
||||||
|
: `${progressData.toFixed(0)}%`
|
||||||
|
}
|
||||||
|
size={200}
|
||||||
|
strokeWidth={10}
|
||||||
|
radius={60}
|
||||||
|
color="rose"
|
||||||
|
>
|
||||||
|
<span className="h-12 w-12 rounded-full bg-rose-100 flex items-center justify-center text-sm text-rose-500 font-medium">
|
||||||
|
{isNaN(progressData) || !isFinite(progressData)
|
||||||
|
? "0%"
|
||||||
|
: `${progressData.toFixed(0)}%`}
|
||||||
|
</span>
|
||||||
|
</ProgressCircle>
|
||||||
|
<br></br>
|
||||||
|
<Legend
|
||||||
|
className="mt-3 mx-auto w-1/2"
|
||||||
|
categories={["Completed Tasks"]}
|
||||||
|
colors={["rose"]}
|
||||||
|
></Legend>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</TabGroup>
|
</TabGroup>
|
||||||
|
|||||||
@ -9,8 +9,17 @@ export function ColumnContainer({ column, createTask, tasks, deleteTask, updateT
|
|||||||
return tasks.map((task) => task.id);
|
return tasks.map((task) => task.id);
|
||||||
}, [tasks]);
|
}, [tasks]);
|
||||||
|
|
||||||
|
const { setNodeRef, attributes, listeners } = useSortable({
|
||||||
|
id: column.id,
|
||||||
|
data: {
|
||||||
|
type: "Column",
|
||||||
|
column,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
className="
|
className="
|
||||||
bg-[#f1f2f4]
|
bg-[#f1f2f4]
|
||||||
w-[280px]
|
w-[280px]
|
||||||
@ -21,6 +30,8 @@ export function ColumnContainer({ column, createTask, tasks, deleteTask, updateT
|
|||||||
">
|
">
|
||||||
{/* Column title */}
|
{/* Column title */}
|
||||||
<div
|
<div
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
className="
|
className="
|
||||||
ml-3
|
ml-3
|
||||||
text-md
|
text-md
|
||||||
|
|||||||
@ -121,6 +121,7 @@ export function KanbanBoard() {
|
|||||||
user: task.user,
|
user: task.user,
|
||||||
list_board: task.list_board,
|
list_board: task.list_board,
|
||||||
tags: task.tags,
|
tags: task.tags,
|
||||||
|
subtaskCount: task.sub_task_count,
|
||||||
}));
|
}));
|
||||||
setTasks(transformedTasks);
|
setTasks(transformedTasks);
|
||||||
|
|
||||||
@ -220,26 +221,43 @@ export function KanbanBoard() {
|
|||||||
if (!over) return; // If not dropped over anything, exit
|
if (!over) return; // If not dropped over anything, exit
|
||||||
|
|
||||||
const activeId = active.id;
|
const activeId = active.id;
|
||||||
const overId = over.id;
|
|
||||||
|
|
||||||
const isActiveATask = active.data.current?.type === "Task";
|
const isActiveATask = active.data.current?.type === "Task";
|
||||||
|
const isOverATask = over.data.current?.type === "Task";
|
||||||
const isOverAColumn = over.data.current?.type === "Column";
|
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
|
// Move tasks between columns and update columnId
|
||||||
if (isActiveATask && isOverAColumn) {
|
if (isActiveATask && isOverAColumn) {
|
||||||
setTasks((tasks) => {
|
setTasks((tasks) => {
|
||||||
const activeIndex = tasks.findIndex((t) => t.id === activeId);
|
const activeIndex = tasks.findIndex((t) => t.id === activeId);
|
||||||
|
const columnId = over.data.current.column.id;
|
||||||
// Extract the column ID from overId
|
|
||||||
const columnId = extractColumnId(overId);
|
|
||||||
|
|
||||||
tasks[activeIndex].columnId = columnId;
|
tasks[activeIndex].columnId = columnId;
|
||||||
|
|
||||||
// API call to update task's columnId
|
// API call to update task's columnId
|
||||||
axiosInstance
|
axiosInstance
|
||||||
.put(`todo/change_task_list_board/`, {
|
.put(`todo/change_task_list_board/`, {
|
||||||
todo_id: activeId,
|
todo_id: activeId,
|
||||||
new_list_board_id: over.data.current.task.columnId,
|
new_list_board_id: columnId,
|
||||||
new_index: 0,
|
new_index: 0,
|
||||||
})
|
})
|
||||||
.then((response) => {})
|
.then((response) => {})
|
||||||
@ -251,15 +269,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
|
// Handle the drag-over event
|
||||||
function onDragOver(event) {
|
function onDragOver(event) {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
@ -286,16 +295,6 @@ export function KanbanBoard() {
|
|||||||
tasks[activeIndex].columnId = tasks[overIndex].columnId;
|
tasks[activeIndex].columnId = tasks[overIndex].columnId;
|
||||||
return arrayMove(tasks, activeIndex, overIndex - 1);
|
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);
|
return arrayMove(tasks, activeIndex, overIndex);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -305,16 +304,6 @@ export function KanbanBoard() {
|
|||||||
if (isActiveATask && isOverAColumn && tasks.some((task) => task.columnId !== overId)) {
|
if (isActiveATask && isOverAColumn && tasks.some((task) => task.columnId !== overId)) {
|
||||||
setTasks((tasks) => {
|
setTasks((tasks) => {
|
||||||
const activeIndex = tasks.findIndex((t) => t.id === activeId);
|
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;
|
tasks[activeIndex].columnId = overId;
|
||||||
return arrayMove(tasks, activeIndex, activeIndex);
|
return arrayMove(tasks, activeIndex, activeIndex);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { BsFillTrashFill } from "react-icons/bs";
|
|
||||||
import { useSortable } from "@dnd-kit/sortable";
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import { TaskDetailModal } from "./taskDetailModal";
|
import { TaskDetailModal } from "./taskDetailModal";
|
||||||
|
import { GoChecklist, GoArchive } from "react-icons/go";
|
||||||
|
|
||||||
export function TaskCard({ task, deleteTask, updateTask }) {
|
export function TaskCard({ task, deleteTask, updateTask }) {
|
||||||
|
// State to track if the mouse is over the task card
|
||||||
const [mouseIsOver, setMouseIsOver] = useState(false);
|
const [mouseIsOver, setMouseIsOver] = useState(false);
|
||||||
|
|
||||||
|
// DnD Kit hook for sortable items
|
||||||
const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
|
const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
|
||||||
id: task.id,
|
id: task.id,
|
||||||
data: {
|
data: {
|
||||||
@ -15,65 +17,161 @@ export function TaskCard({ task, deleteTask, updateTask }) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Style for the task card, adjusting for dragging animation
|
||||||
const style = {
|
const style = {
|
||||||
transition,
|
transition,
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
// ---- DESC AND TAG ---- */
|
||||||
/* If card is dragged */
|
|
||||||
|
if (task.tags === undefined) {
|
||||||
|
task.tags = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
const tags =
|
||||||
|
task.tags.length > 0 ? (
|
||||||
|
<div className="flex flex-wrap mx-3 mt-4">
|
||||||
|
{task.tags.map((tag, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
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`}>
|
||||||
|
<p className="text-[9px] truncate">{tag}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
// difficulty?
|
||||||
|
const difficultyTag = task.difficulty ? (
|
||||||
|
<span
|
||||||
|
className={`text-[9px] inline-flex items-center font-bold leading-sm uppercase px-2 py-1 rounded-full ${
|
||||||
|
task.difficulty === 1
|
||||||
|
? "bg-blue-200 text-blue-700"
|
||||||
|
: task.difficulty === 2
|
||||||
|
? "bg-green-200 text-green-700"
|
||||||
|
: task.difficulty === 3
|
||||||
|
? "bg-yellow-200 text-yellow-700"
|
||||||
|
: task.difficulty === 4
|
||||||
|
? "bg-red-200 text-red-700"
|
||||||
|
: "bg-purple-200 text-purple-700"
|
||||||
|
}`}>
|
||||||
|
difficulty
|
||||||
|
</span>
|
||||||
|
) : 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 (
|
||||||
|
<span className={`bg-${colorClass} text-[10px] font-xl font-bold px-2 py-1 rounded-full`}>
|
||||||
|
Due: {formattedDueDate}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Subtask count
|
||||||
|
const subtaskCountTag = task.subtaskCount ? (
|
||||||
|
<span className="flex flex-row items-center bg-green-200 text-green-600 text-[10px] font-xl font-bold me-2 px-2.5 py-0.5 rounded">
|
||||||
|
<GoChecklist /> {task.subtaskCount}
|
||||||
|
</span>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
// ---- DRAG STATE ---- */
|
||||||
|
|
||||||
|
// If the card is being dragged
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={style}
|
style={style}
|
||||||
className="
|
className="opacity-30 bg-mainBackgroundColor p-2.5 items-center flex text-left rounded-xl border-2 border-gray-400 cursor-grab relative"
|
||||||
opacity-30
|
|
||||||
bg-mainBackgroundColor p-2.5 items-center flex text-left rounded-xl border-2 border-gray-400 cursor-grab relative
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the card is not being dragged
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{/* Task Detail Modal */}
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
taskId={task.id}
|
taskId={task.id}
|
||||||
title={task.content}
|
title={task.content}
|
||||||
description={task.description}
|
description={task.description}
|
||||||
tags={task.tags}
|
tags={task.tags}
|
||||||
difficulty={task.difficulty}
|
difficulty={task.difficulty}
|
||||||
f challenge={task.challenge}
|
challenge={task.challenge}
|
||||||
importance={task.importance}
|
importance={task.importance}
|
||||||
|
updateTask={updateTask}
|
||||||
|
completed={task.completed}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* -------- Task Card -------- */}
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
style={style}
|
style={style}
|
||||||
className="justify-center items-center flex text-left rounded-xl cursor-grab relative hover:border-2 hover:border-blue-400 shadow bg-white"
|
className="justify-center flex flex-col text-left rounded-xl cursor-grab relative hover:border-2 hover:border-blue-400 shadow bg-white"
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
setMouseIsOver(true);
|
setMouseIsOver(true);
|
||||||
}}
|
}}
|
||||||
onMouseLeave={() => {
|
onMouseLeave={() => {
|
||||||
setMouseIsOver(false);
|
setMouseIsOver(false);
|
||||||
}}>
|
}}
|
||||||
<p
|
onClick={() => document.getElementById(`task_detail_modal_${task.id}`).showModal()}>
|
||||||
className="p-2.5 my-auto w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-xl shadow bg-white"
|
{/* -------- Task Content -------- */}
|
||||||
onClick={() => document.getElementById(`task_detail_modal_${task.id}`).showModal()}>
|
{/* Tags */}
|
||||||
{task.content}
|
{tags}
|
||||||
</p>
|
<div>
|
||||||
|
{/* Title */}
|
||||||
{mouseIsOver && (
|
<p
|
||||||
<button
|
className={`p-2.5 my-auto w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-xl bg-white font-semibold`}
|
||||||
onClick={() => {
|
onClick={() => document.getElementById(`task_detail_modal_${task.id}`).showModal()}>
|
||||||
deleteTask(task.id);
|
{task.content}
|
||||||
}}
|
</p>
|
||||||
className="stroke-white absolute right-0 top-1/2 rounded-full bg-white -translate-y-1/2 bg-columnBackgroundColor p-2 hover:opacity-100 ">
|
{/* -------- Archive Task Button -------- */}
|
||||||
<BsFillTrashFill />
|
{mouseIsOver && (
|
||||||
</button>
|
<button
|
||||||
)}
|
onClick={() => {
|
||||||
|
deleteTask(task.id);
|
||||||
|
}}
|
||||||
|
className="stroke-white absolute right-0 top-1/2 rounded-full bg-white -translate-y-1/2 bg-columnBackgroundColor p-2 hover:opacity-100 ">
|
||||||
|
<GoArchive />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Description */}
|
||||||
|
<div className="flex flex-wrap mb-4 mx-3 space-x-1">
|
||||||
|
{difficultyTag}
|
||||||
|
{dueDateTag}
|
||||||
|
{subtaskCountTag}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,41 +1,283 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { FaTasks, FaRegListAlt } from "react-icons/fa";
|
import { FaTasks, FaRegListAlt } from "react-icons/fa";
|
||||||
import { FaPlus } from "react-icons/fa6";
|
import { FaPlus, FaRegTrashCan, FaPencil } from "react-icons/fa6";
|
||||||
import { TbChecklist } from "react-icons/tb";
|
import { TbChecklist } from "react-icons/tb";
|
||||||
|
import DatePicker from "react-datepicker";
|
||||||
|
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 }) {
|
export function TaskDetailModal({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
tags,
|
||||||
|
difficulty,
|
||||||
|
challenge,
|
||||||
|
importance,
|
||||||
|
taskId,
|
||||||
|
updateTask,
|
||||||
|
completed,
|
||||||
|
}) {
|
||||||
const [isChallengeChecked, setChallengeChecked] = useState(challenge);
|
const [isChallengeChecked, setChallengeChecked] = useState(challenge);
|
||||||
const [isImportantChecked, setImportantChecked] = useState(importance);
|
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(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 handleChallengeChange = () => {
|
const handleTitleChange = async () => {
|
||||||
|
const data = {
|
||||||
|
title: currentTitle,
|
||||||
|
};
|
||||||
|
await updateTodoTaskPartial(taskId, data);
|
||||||
|
setTitleEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
setChallengeChecked(!isChallengeChecked);
|
||||||
|
const data = {
|
||||||
|
challenge: !isChallengeChecked,
|
||||||
|
};
|
||||||
|
await updateTodoTaskPartial(taskId, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImportantChange = () => {
|
const handleImportantChange = async () => {
|
||||||
setImportantChecked(!isImportantChecked);
|
setImportantChecked(!isImportantChecked);
|
||||||
|
const data = {
|
||||||
|
important: !isImportantChecked,
|
||||||
|
};
|
||||||
|
await updateTodoTaskPartial(taskId, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDifficultyChange = (event) => {
|
const handleDifficultyChange = async (event) => {
|
||||||
setCurrentDifficulty(parseInt(event.target.value, 10));
|
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 = () => {
|
||||||
|
if (!isTaskComplete) {
|
||||||
|
setStartDateEnabled(!startDateEnabled);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEndDateChange = () => {
|
||||||
|
if (!isTaskComplete) {
|
||||||
|
setEndDateEnabled(!endDateEnabled);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 () => {
|
||||||
|
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) => (
|
||||||
|
<div key={index} className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={subtask.completed}
|
||||||
|
className="checkbox checkbox-xs bg-gray-400"
|
||||||
|
onChange={() => toggleSubtaskCompletion(index)}
|
||||||
|
/>
|
||||||
|
<div className={`flex items-center rounded p-2 shadow border-2 ${subtask.completed && "line-through"}`}>
|
||||||
|
{subtask.description}
|
||||||
|
<FaRegTrashCan className="cursor-pointer ml-2 text-red-500" onClick={() => deleteSubtask(index)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
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) => (
|
||||||
|
<div
|
||||||
|
key={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.name}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
// Selected tags
|
||||||
|
const selectedTagElements = selectedTags.map((tag, index) => (
|
||||||
|
<div
|
||||||
|
key={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.name}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dialog id={`task_detail_modal_${taskId}`} className="modal">
|
<dialog id={`task_detail_modal_${taskId}`} className="modal">
|
||||||
<div className="modal-box w-4/5 max-w-3xl">
|
<div className="modal-box w-4/5 max-w-3xl">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div className="flex flex-col py-2">
|
<div className="flex flex-col py-2">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<h3 className="font-bold text-lg">
|
{isTitleEditing ? (
|
||||||
<span className="flex gap-2">
|
<div className="flex gap-2 items-center">
|
||||||
{<FaTasks className="my-2" />}
|
<FaTasks className="my-2" />
|
||||||
{title}
|
<input
|
||||||
</span>
|
type="text"
|
||||||
</h3>
|
className="input-md input-bordered font-bold text-lg"
|
||||||
<p className="text-xs">{title}</p>
|
value={currentTitle}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button className="btn btn-sm" onClick={handleTitleChange}>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<h3 className="font-bold text-lg">
|
||||||
|
<span className="flex gap-2">
|
||||||
|
{<FaTasks className="my-2" />}
|
||||||
|
{currentTitle}
|
||||||
|
<FaPencil className="my-2" onClick={() => setTitleEditing(true)} />
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
|
<p className="text-xs">{currentTitle}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tags */}
|
{/* Tags */}
|
||||||
<div className="flex flex-col py-2 pb-4">
|
<div className="flex flex-col py-2 pb-4">
|
||||||
<div className="flex flex-row space-x-5">
|
<div className="flex flex-row space-x-5">
|
||||||
@ -43,19 +285,89 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
|
|||||||
<label tabIndex={0} className="btn-md border-2 rounded-xl m-1 py-1">
|
<label tabIndex={0} className="btn-md border-2 rounded-xl m-1 py-1">
|
||||||
+ Add Tags
|
+ Add Tags
|
||||||
</label>
|
</label>
|
||||||
<ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
|
<ul tabIndex={0} className="dropdown-content z-[10] menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||||
<li>
|
{tags.map((tag, index) => (
|
||||||
<a>
|
<li key={index}>
|
||||||
<input type="checkbox" checked="checked" className="checkbox checkbox-sm" />
|
<label className="cursor-pointer space-x-2">
|
||||||
Item 2
|
<input
|
||||||
</a>
|
type="checkbox"
|
||||||
</li>
|
checked={selectedTags.includes(tag)}
|
||||||
|
className="checkbox checkbox-sm"
|
||||||
|
onChange={() => handleTagChange(tag)}
|
||||||
|
/>
|
||||||
|
{tag}
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-nowrap overflow-x-auto"></div>
|
<div className="flex flex-nowrap overflow-x-auto">
|
||||||
|
{existingTags}
|
||||||
|
{selectedTagElements}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Date Picker */}
|
||||||
|
<div className="flex flex-col space-y-2 mb-2">
|
||||||
|
{/* Start */}
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-bold">Start At</p>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={startDateEnabled}
|
||||||
|
className="checkbox checkbox-xs bg-gray-400"
|
||||||
|
onChange={handleStartDateChange}
|
||||||
|
/>
|
||||||
|
<div className={`rounded p-2 shadow border-2 ${!startDateEnabled && "opacity-50"}`}>
|
||||||
|
<DatePicker selected={dateStart} onChange={handleStartDateValueChange} disabled={!startDateEnabled} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Start event time picker */}
|
||||||
|
<div className="rounded p-2 shadow border-2 ml-2 mt-4">
|
||||||
|
{/* handleStartEventTimeChange */}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="10:00 AM"
|
||||||
|
className="input input-bordered w-full max-w-xs"
|
||||||
|
onClick={handleStartEventTimeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Complete? */}
|
||||||
|
<div className="mx-4">
|
||||||
|
<div className="flex items-center space-x-2 mt-4">
|
||||||
|
<div className="flex-1 flex-row card shadow border-2 p-2 pr-2">
|
||||||
|
<p className="text-md mx-2">Complete</p>
|
||||||
|
<input type="checkbox" checked={isTaskComplete} className="checkbox checkbox-xl bg-gray-400" />
|
||||||
|
<button className="btn btn-sm mt-2" onClick={handleStartEventTimeChange}>
|
||||||
|
Update Start Time
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* End */}
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-bold">End At</p>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={endDateEnabled}
|
||||||
|
className="checkbox checkbox-xs bg-gray-400"
|
||||||
|
onChange={handleEndDateChange}
|
||||||
|
/>
|
||||||
|
<div className={`rounded p-2 shadow border-2 ${!endDateEnabled && "opacity-50"}`}>
|
||||||
|
<DatePicker selected={dateEnd} onChange={handleEndDateValueChange} disabled={!endDateEnabled} />
|
||||||
|
</div>
|
||||||
|
{/* End event time picker */}
|
||||||
|
<div className="rounded p-2 shadow border-2">this is time picker</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h2 className="font-bold">
|
<h2 className="font-bold">
|
||||||
@ -68,7 +380,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
|
|||||||
{description}
|
{description}
|
||||||
</textarea>
|
</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Difficulty, Challenge, and Importance */}
|
{/* Difficulty, Challenge, and Importance */}
|
||||||
<div className="flex flex-row space-x-3 my-4">
|
<div className="flex flex-row space-x-3 my-4">
|
||||||
<div className="flex-1 card shadow border-2 p-2">
|
<div className="flex-1 card shadow border-2 p-2">
|
||||||
@ -98,7 +409,7 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isChallengeChecked}
|
checked={isChallengeChecked}
|
||||||
className="checkbox"
|
className="checkbox bg-black"
|
||||||
onChange={handleChallengeChange}
|
onChange={handleChallengeChange}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@ -113,14 +424,13 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isImportantChecked}
|
checked={isImportantChecked}
|
||||||
className="checkbox"
|
className="checkbox bg-black"
|
||||||
onChange={handleImportantChange}
|
onChange={handleImportantChange}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Subtask */}
|
{/* Subtask */}
|
||||||
<div className="flex flex-col pt-2">
|
<div className="flex flex-col pt-2">
|
||||||
<h2 className="font-bold">
|
<h2 className="font-bold">
|
||||||
@ -130,14 +440,21 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
|
|||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex space-x-3 pt-2">
|
<div className="flex space-x-3 pt-2">
|
||||||
<input type="text" placeholder="subtask topic" className="input input-bordered flex-1 w-full" />
|
<input
|
||||||
<button className="btn">
|
type="text"
|
||||||
|
placeholder="subtask topic"
|
||||||
|
className="input input-bordered flex-1 w-full"
|
||||||
|
value={subtaskText}
|
||||||
|
onChange={(e) => setSubtaskText(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button className="btn" onClick={addSubtask}>
|
||||||
<FaPlus />
|
<FaPlus />
|
||||||
Add Subtask
|
Add Subtask
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Display Subtasks */}
|
||||||
|
<div className="flex flex-col space-y-2 pt-2">{subtaskElements}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">X</button>
|
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">X</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { apiUserLogout } from "src/api/AuthenticationApi";
|
import { apiUserLogout } from "src/api/AuthenticationApi";
|
||||||
import { useAuth } from "src/hooks/AuthHooks";
|
import { useAuth } from "src/hooks/AuthHooks";
|
||||||
|
import { axiosInstance } from "src/api/AxiosConfig";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
Profile: "/profile",
|
Profile: "/profile",
|
||||||
@ -10,6 +12,7 @@ const settings = {
|
|||||||
export function NavBar() {
|
export function NavBar() {
|
||||||
const Navigate = useNavigate();
|
const Navigate = useNavigate();
|
||||||
const { isAuthenticated, setIsAuthenticated } = useAuth();
|
const { isAuthenticated, setIsAuthenticated } = useAuth();
|
||||||
|
const [profile_pic, setProfilePic] = useState(undefined);
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
apiUserLogout();
|
apiUserLogout();
|
||||||
@ -17,6 +20,25 @@ export function NavBar() {
|
|||||||
Navigate("/");
|
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 (
|
return (
|
||||||
<div data-theme="night" className="navbar bg-base-100">
|
<div data-theme="night" className="navbar bg-base-100">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@ -32,19 +54,23 @@ export function NavBar() {
|
|||||||
<div className="dropdown dropdown-end">
|
<div className="dropdown dropdown-end">
|
||||||
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
|
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
|
||||||
<div className="w-10 rounded-full">
|
<div className="w-10 rounded-full">
|
||||||
<img src="https://upload.wikimedia.org/wikipedia/commons/8/89/Portrait_Placeholder.png" />
|
<img src={profile_pic} />
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<ul
|
<ul
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="mt-3 z-[10] p-2 shadow menu menu-sm dropdown-content bg-base-100 rounded-box w-52">
|
className="mt-3 z-[10] p-2 shadow menu menu-sm dropdown-content bg-base-100 rounded-box w-52"
|
||||||
|
>
|
||||||
<li>
|
<li>
|
||||||
<a href={settings.Profile} className="justify-between">
|
<a
|
||||||
|
onClick={() => Navigate(settings.Profile)}
|
||||||
|
className="justify-between"
|
||||||
|
>
|
||||||
Profile
|
Profile
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href={settings.Account}>Settings</a>
|
<a onClick={() => Navigate(settings.Account)}>Settings</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a onClick={logout}>Logout</a>
|
<a onClick={logout}>Logout</a>
|
||||||
@ -53,10 +79,16 @@ export function NavBar() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button className="btn btn-outline btn-info" onClick={() => Navigate("/login")}>
|
<button
|
||||||
|
className="btn btn-outline btn-info"
|
||||||
|
onClick={() => Navigate("/login")}
|
||||||
|
>
|
||||||
Login
|
Login
|
||||||
</button>
|
</button>
|
||||||
<button className="btn btn-success" onClick={() => Navigate("/signup")}>
|
<button
|
||||||
|
className="btn btn-success"
|
||||||
|
onClick={() => Navigate("/signup")}
|
||||||
|
>
|
||||||
Sign Up
|
Sign Up
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,13 +1,30 @@
|
|||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { ApiUpdateUserProfile } from "src/api/UserProfileApi";
|
import { ApiUpdateUserProfile } from "src/api/UserProfileApi";
|
||||||
|
import { axiosInstance } from "src/api/AxiosConfig";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export function ProfileUpdateComponent() {
|
export function ProfileUpdateComponent() {
|
||||||
const [file, setFile] = useState(null);
|
const [file, setFile] = useState(null);
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUserName] = useState("");
|
||||||
const [fullName, setFullName] = useState("");
|
const [about, setAbout] = useState();
|
||||||
const [about, setAbout] = useState("");
|
|
||||||
const defaultImage = "https://i1.sndcdn.com/artworks-cTz48e4f1lxn5Ozp-L3hopw-t500x500.jpg";
|
|
||||||
const fileInputRef = useRef(null);
|
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.username;
|
||||||
|
const fetchedAbout = response.data.about;
|
||||||
|
setProfilePic(fetchedProfilePic);
|
||||||
|
setAbout(fetchedAbout);
|
||||||
|
setUserName(fetchedName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching user:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchUser();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleImageUpload = () => {
|
const handleImageUpload = () => {
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
@ -25,7 +42,7 @@ export function ProfileUpdateComponent() {
|
|||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("profile_pic", file);
|
formData.append("profile_pic", file);
|
||||||
formData.append("first_name", username);
|
formData.append("username", username);
|
||||||
formData.append("about", about);
|
formData.append("about", about);
|
||||||
|
|
||||||
ApiUpdateUserProfile(formData);
|
ApiUpdateUserProfile(formData);
|
||||||
@ -50,7 +67,7 @@ export function ProfileUpdateComponent() {
|
|||||||
<img src={URL.createObjectURL(file)} alt="Profile" className="rounded-full" />
|
<img src={URL.createObjectURL(file)} alt="Profile" className="rounded-full" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<img src={defaultImage} alt="Default" className="rounded-full" />
|
<img src={profile_pic} alt="Default" className="rounded-full" />
|
||||||
<i className="fas fa-camera text-white text-2xl absolute bottom-0 right-0 mr-2 mb-2"></i>
|
<i className="fas fa-camera text-white text-2xl absolute bottom-0 right-0 mr-2 mb-2"></i>
|
||||||
<i className="fas fa-arrow-up text-white text-2xl absolute top-0 right-0 mr-2 mt-2"></i>
|
<i className="fas fa-arrow-up text-white text-2xl absolute top-0 right-0 mr-2 mt-2"></i>
|
||||||
</>
|
</>
|
||||||
@ -58,7 +75,7 @@ export function ProfileUpdateComponent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Username Field */}
|
{/* Username Field
|
||||||
<div className="w-96">
|
<div className="w-96">
|
||||||
<label className="block mb-2 text-gray-600">Username</label>
|
<label className="block mb-2 text-gray-600">Username</label>
|
||||||
<input
|
<input
|
||||||
@ -68,17 +85,17 @@ export function ProfileUpdateComponent() {
|
|||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Full Name Field */}
|
{/* Full Name Field */}
|
||||||
<div className="w-96">
|
<div className="w-96">
|
||||||
<label className="block mb-2 text-gray-600">Full Name</label>
|
<label className="block mb-2 text-gray-600">username</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter your full name"
|
placeholder="Enter your username"
|
||||||
className="input w-full"
|
className="input w-full"
|
||||||
value={fullName}
|
value={username}
|
||||||
onChange={(e) => setFullName(e.target.value)}
|
onChange={(e) => setUserName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,36 +1,66 @@
|
|||||||
import { ProfileUpdateComponent } from "./ProfileUpdateComponent";
|
import { ProfileUpdateComponent } from "./ProfileUpdateComponent";
|
||||||
|
import { axiosInstance } from "src/api/AxiosConfig";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export function ProfileUpdatePage() {
|
export function ProfileUpdatePage() {
|
||||||
|
const [profile_pic, setProfilePic] = useState(undefined);
|
||||||
|
const [about, setAbout] = 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;
|
||||||
|
const fetchedUsernames = response.data.username;
|
||||||
|
setProfilePic(fetchedProfilePic);
|
||||||
|
setAbout(fetchedAbout);
|
||||||
|
setUsernames(fetchedUsernames);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching user:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchUser();
|
||||||
|
}, []);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="stats shadow mt-3">
|
<div className="stats shadow mt-3">
|
||||||
<div className="stat">
|
<div className="stat">
|
||||||
<div className="stat-title truncate">Username</div>
|
<div className="stat-title truncate">Username</div>
|
||||||
<div className="stat-value truncate">Sirin</div>
|
<div className="stat-value truncate">{username}</div>
|
||||||
<div className="stat-desc truncate">User ID</div>
|
{/* <div className="stat-desc truncate">User ID</div> */}
|
||||||
<div className="stat-figure text-secondary">
|
<div className="stat-figure text-secondary">
|
||||||
<div className="avatar online">
|
<div className="avatar online">
|
||||||
<div className="w-20 rounded-full">
|
<div className="w-20 rounded-full">
|
||||||
<img src="https://us-tuna-sounds-images.voicemod.net/f322631f-689a-43ac-81ab-17a70f27c443-1692187175560.png" />
|
<img src={profile_pic} alt="Profile Picture" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stat">
|
{/* <div className="stat">
|
||||||
<div className="stat-title">Health</div>
|
<div className="stat-title">Health</div>
|
||||||
<div className="stat-value flex truncate">
|
<div className="stat-value flex truncate">
|
||||||
234/3213
|
234/3213
|
||||||
<div className="stat-figure text-secondary px-2">
|
<div className="stat-figure text-secondary px-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="red" viewBox="0 0 24 24" className="inline-block w-8 h-8">
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="red"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="inline-block w-8 h-8"
|
||||||
|
>
|
||||||
<path d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0"></path>
|
<path d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-desc py-2">32% Remain</div>
|
<div className="stat-desc py-2">32% Remain</div>
|
||||||
<progress className="progress progress-error w-56" value={20} max="100"></progress>
|
<progress
|
||||||
</div>
|
className="progress progress-error w-56"
|
||||||
|
value={20}
|
||||||
|
max="100"
|
||||||
|
></progress>
|
||||||
|
</div> */}
|
||||||
|
{/*
|
||||||
<div className="stat">
|
<div className="stat">
|
||||||
<div className="stat-title truncate">Level</div>
|
<div className="stat-title truncate">Level</div>
|
||||||
<div className="stat-value flex">
|
<div className="stat-value flex">
|
||||||
@ -40,13 +70,18 @@ export function ProfileUpdatePage() {
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="#3abff8"
|
fill="#3abff8"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
className="inline-block w-8 h-8">
|
className="inline-block w-8 h-8"
|
||||||
|
>
|
||||||
<path d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
<path d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-desc py-2">3213/321312321 points</div>
|
<div className="stat-desc py-2">3213/321312321 points</div>
|
||||||
<progress className="progress progress-info w-36" value="10" max="100"></progress>
|
<progress
|
||||||
|
className="progress progress-info w-36"
|
||||||
|
value="10"
|
||||||
|
max="100"
|
||||||
|
></progress>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stat">
|
<div className="stat">
|
||||||
@ -58,34 +93,40 @@ export function ProfileUpdatePage() {
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
className="inline-block w-8 h-8 stroke-current">
|
className="inline-block w-8 h-8 stroke-current"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
stroke="gold"
|
stroke="gold"
|
||||||
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"></path>
|
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
|
||||||
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-desc py-2">Top 12% of Global Ranking</div>
|
<div className="stat-desc py-2">Top 12% of Global Ranking</div>
|
||||||
<progress className="progress progress-warning w-56" value={20} max="100"></progress>
|
<progress
|
||||||
</div>
|
className="progress progress-warning w-56"
|
||||||
|
value={20}
|
||||||
|
max="100"
|
||||||
|
></progress>
|
||||||
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card bg-base-100 shadow">
|
<div className="card bg-base-100 shadow">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h2 className="card-title">About me</h2>
|
<h2 className="card-title">About me</h2>
|
||||||
<div className="card-actions justify-end"></div>
|
<div className="card-actions justify-end"></div>
|
||||||
<textarea className="textarea textarea-bordered textarea-lg w-full" disabled>
|
<textarea
|
||||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nostrum dolores recusandae, officiis consequuntur
|
className="textarea textarea-bordered textarea-lg w-full"
|
||||||
nam, non ab commodi totam mollitia iusto nemo voluptatum error aliquam similique perspiciatis, eligendi
|
disabled
|
||||||
nulla. Animi, sit?
|
placeholder="Enter your about me"
|
||||||
</textarea>
|
value={about}
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* <div className="grid grid-cols-2 grid-rows-2 gap-4 my-2">
|
||||||
<div className="grid grid-cols-2 grid-rows-2 gap-4 my-2">
|
|
||||||
<div className="col-span-full">
|
<div className="col-span-full">
|
||||||
<div className="card bg-base-100 shadow">
|
<div className="card bg-base-100 shadow">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
@ -110,18 +151,21 @@ export function ProfileUpdatePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className="fixed bottom-4 right-4">
|
<div className="fixed bottom-4 right-4">
|
||||||
<ul className="menu menu-horizontal bg-base-200 rounded-box">
|
<ul className="menu menu-horizontal bg-base-200 rounded-box">
|
||||||
<li>
|
<li>
|
||||||
<a onClick={() => document.getElementById("my_modal_4").showModal()}>
|
<a
|
||||||
|
onClick={() => document.getElementById("my_modal_4").showModal()}
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
className="h-5 w-5"
|
className="h-5 w-5"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
stroke="currentColor">
|
stroke="currentColor"
|
||||||
|
>
|
||||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
|
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
|
||||||
</svg>
|
</svg>
|
||||||
<p className="text-xl font-bold">Edit</p>
|
<p className="text-xl font-bold">Edit</p>
|
||||||
@ -135,7 +179,9 @@ export function ProfileUpdatePage() {
|
|||||||
<div className="modal-box w-11/12 max-w-5xl flex flex-col">
|
<div className="modal-box w-11/12 max-w-5xl flex flex-col">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<ProfileUpdateComponent />
|
<ProfileUpdateComponent />
|
||||||
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user