Merge pull request #88 from TurTaskProject/main

Release large kanban and profile update bug fixes
This commit is contained in:
Sirin Puenggun 2023-11-28 12:05:28 +07:00 committed by GitHub
commit 52781cfec6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1568 additions and 466 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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']

View File

@ -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)

View File

@ -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)),
] ]

View File

@ -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]

View File

@ -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,)

View File

@ -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:

View File

@ -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

View File

@ -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(),
)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View 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

View File

@ -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,

View File

@ -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)),

View File

@ -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):
""" """

View File

@ -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(),
)

View File

@ -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)

View File

@ -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"),
] ]

View File

@ -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)

View File

@ -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"

View File

@ -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:

View 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;
}
};

View File

@ -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);

View File

@ -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);

View File

@ -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>

View File

@ -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}'`)) {

View File

@ -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>

View File

@ -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"
/> />
); );
} }

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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);
}); });

View File

@ -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>
); );

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>