Merge branch 'feature/kanban-board' of https://github.com/TurTaskProject/TurTaskWeb into feature/kanban-board

This commit is contained in:
Pattadon 2023-11-27 12:59:21 +07:00
commit 8f2e64b5e0
24 changed files with 596 additions and 297 deletions

View File

@ -3,7 +3,4 @@ from django.apps import AppConfig
class BoardsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'boards'
def ready(self):
import boards.signals
name = 'boards'

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',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'],
}
REST_USE_JWT = True

View File

@ -88,6 +88,7 @@ SPECTACULAR_SETTINGS = {
'DESCRIPTION': 'API documentation for TurTask',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'],
}
REST_USE_JWT = True

View File

@ -1,32 +1,35 @@
from django.test import TestCase
from rest_framework.test import APITestCase
from django.urls import reverse
from tasks.models import Todo
from django.utils import timezone
from datetime import timedelta
from tasks.tests.utils import create_test_user, login_user
from boards.models import Board
from tasks.tests.utils import create_test_user
class DashboardStatsAndWeeklyViewSetTests(TestCase):
class DashboardStatsAndWeeklyViewSetTests(APITestCase):
def setUp(self):
self.user = create_test_user()
self.client = login_user(self.user)
self.client.force_authenticate(user=self.user)
self.list_board = Board.objects.get(user=self.user).listboard_set.first()
def create_task(self, title, completed=False, completion_date=None, end_event=None):
def _create_task(self, title, completed=False, completion_date=None, end_event=None):
return Todo.objects.create(
user=self.user,
title=title,
completed=completed,
completion_date=completion_date,
end_event=end_event
end_event=end_event,
list_board=self.list_board
)
def test_dashboard_stats_view(self):
# Create tasks for testing
self.create_task('Task 1', completed=True)
self.create_task('Task 2', end_event=timezone.now() - timedelta(days=8))
self.create_task('Task 3', end_event=timezone.now())
self._create_task('Task 1', completed=True)
self._create_task('Task 2', end_event=timezone.now() - timedelta(days=8))
self._create_task('Task 3', end_event=timezone.now())
response = self.client.get(reverse('stats-list'))
response = self.client.get(reverse('statstodo-list'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['completed_this_week'], 1)
@ -35,69 +38,9 @@ class DashboardStatsAndWeeklyViewSetTests(TestCase):
def test_dashboard_weekly_view(self):
# Create tasks for testing
self.create_task('Task 1', completion_date=timezone.now() - timedelta(days=1))
self.create_task('Task 2', end_event=timezone.now() - timedelta(days=8))
self.create_task('Task 3', end_event=timezone.now())
self._create_task('Task 1', completion_date=timezone.now() - timedelta(days=1))
self._create_task('Task 2', end_event=timezone.now() - timedelta(days=8))
self._create_task('Task 3', end_event=timezone.now())
response = self.client.get(reverse('weekly-list'))
self.assertEqual(response.status_code, 200)
# class DashboardStatsAPITestCase(TestCase):
# def setUp(self):
# # Create a test user
# self.user = create_test_user()
# # Create test tasks
# self.todo = Todo.objects.create(user=self.user, title='Test Todo')
# self.recurrence_task = RecurrenceTask.objects.create(user=self.user, title='Test Recurrence Task')
# # Create an API client
# self.client = APIClient()
# def test_dashboard_stats_api(self):
# # Authenticate the user
# self.client.force_authenticate(user=self.user)
# # Make a GET request to the DashboardStatsAPIView
# response = self.client.get(reverse("dashboard-stats"))
# # Assert the response status code is 200
# self.assertEqual(response.status_code, 200)
# def test_task_completion_status_update(self):
# # Authenticate the user
# self.client.force_authenticate(user=self.user)
# # Make a POST request to update the completion status of a task
# data = {'task_id': self.todo.id, 'is_completed': True}
# response = self.client.post(reverse("dashboard-stats"), data, format='json')
# # Assert the response status code is 200
# self.assertEqual(response.status_code, 200)
# # Assert the message in the response
# self.assertEqual(response.data['message'], 'Task completion status updated successfully')
# # Refresh the todo instance from the database and assert the completion status
# self.todo.refresh_from_db()
# self.assertTrue(self.todo.completed)
# class WeeklyStatsAPITestCase(TestCase):
# def setUp(self):
# # Create a test user
# self.user = create_test_user()
# # Create an API client
# self.client = APIClient()
# def test_weekly_stats_api(self):
# # Authenticate the user
# self.client.force_authenticate(user=self.user)
# # Make a GET request to the WeeklyStatsAPIView
# response = self.client.get(reverse('dashboard-weekly-stats'))
# # Assert the response status code is 200
# self.assertEqual(response.status_code, 200)

View File

@ -1,5 +1,6 @@
from rest_framework import serializers
from .models import Todo, RecurrenceTask
from boards.models import Board
from tasks.models import Todo, RecurrenceTask
class GoogleCalendarEventSerializer(serializers.Serializer):
@ -17,18 +18,22 @@ class TodoUpdateSerializer(serializers.ModelSerializer):
updated = serializers.DateTimeField(source="last_update")
start_datetime = serializers.DateTimeField(source="start_event", required=False)
end_datetime = serializers.DateTimeField(source="end_event", required=False)
list_board = serializers.SerializerMethodField()
class Meta:
model = Todo
fields = ('id', 'summary', 'description', 'created', 'updated', 'start_datetime', 'end_datetime')
fields = ('id', 'summary', 'description', 'created', 'updated', 'start_datetime', 'end_datetime', 'list_board')
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super(TodoUpdateSerializer, self).__init__(*args, **kwargs)
def get_list_board(self, obj):
return Board.objects.get(user=self.user).listboard_set.first()
def create(self, validated_data):
validated_data['user'] = self.user
validated_data['list_board'] = self.get_list_board(self)
task = Todo.objects.create(**validated_data)
return task

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.utils import timezone
from boards.models import ListBoard, Board
from tasks.models import Todo
@ -24,66 +23,4 @@ def update_priority(sender, instance, **kwargs):
elif time_until_due <= urgency_threshold and instance.importance < importance_threshold:
instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT
else:
instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT
# @receiver(post_save, sender=Todo)
# def assign_todo_to_listboard(sender, instance, created, **kwargs):
# """Signal handler to automatically assign a Todo to the first ListBoard in the user's Board upon creation."""
# if created:
# user_board = instance.user.board_set.first()
# if user_board:
# first_list_board = user_board.listboard_set.order_by('position').first()
# if first_list_board:
# instance.list_board = first_list_board
# instance.save()
@receiver(post_save, sender=ListBoard)
def create_placeholder_tasks(sender, instance, created, **kwargs):
"""
Signal handler to create placeholder tasks for each ListBoard.
"""
if created:
list_board_position = instance.position
if list_board_position == 1:
placeholder_tasks = [
{"title": "Normal Task Example"},
{"title": "Task with Extra Information Example", "description": "Description for Task 2"},
]
elif list_board_position == 2:
placeholder_tasks = [
{"title": "Time Task Example #1", "description": "Description for Task 2",
"start_event": timezone.now(), "end_event": timezone.now() + timezone.timedelta(days=5)},
]
elif list_board_position == 3:
placeholder_tasks = [
{"title": "Time Task Example #2", "description": "Description for Task 2",
"start_event": timezone.now(), "end_event": timezone.now() + timezone.timedelta(days=30)},
]
elif list_board_position == 4:
placeholder_tasks = [
{"title": "Completed Task Example", "description": "Description for Task 2",
"start_event": timezone.now(), "completed": True},
]
else:
placeholder_tasks = [
{"title": "Default Task Example"},
]
for task_data in placeholder_tasks:
Todo.objects.create(
list_board=instance,
user=instance.board.user,
title=task_data["title"],
notes=task_data.get("description", ""),
is_active=True,
start_event=task_data.get("start_event"),
end_event=task_data.get("end_event"),
completed=task_data.get("completed", False),
creation_date=timezone.now(),
last_update=timezone.now(),
)
instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT

View File

@ -1,4 +1,5 @@
from rest_framework import serializers
from users.models import CustomUser
from boards.models import ListBoard
from tasks.models import Todo, RecurrenceTask, Habit
@ -8,7 +9,14 @@ class TaskSerializer(serializers.ModelSerializer):
fields = '__all__'
def create(self, validated_data):
# Create a new task with validated data
user_id = validated_data.get('user')
try:
user = CustomUser.objects.get(id=user_id)
except CustomUser.DoesNotExist:
raise serializers.ValidationError("User with the provided ID does not exist.")
validated_data['user'] = user
return Todo.objects.create(**validated_data)
class TaskCreateSerializer(serializers.ModelSerializer):

View File

@ -1,18 +1,22 @@
from datetime import datetime
from zoneinfo import ZoneInfo
from django.test import TestCase
from rest_framework.test import APITestCase
from django.utils import timezone
from tasks.tests.utils import create_test_user, login_user
from tasks.tests.utils import create_test_user
from tasks.serializers import TodoUpdateSerializer
from tasks.models import Todo
from boards.models import Board
class TaskUpdateSerializerTest(TestCase):
class TaskUpdateSerializerTest(APITestCase):
def setUp(self):
self.user = create_test_user()
self.client.force_authenticate(user=self.user)
self.current_time = '2020-08-01T00:00:00Z'
self.end_time = '2020-08-01T00:00:00Z'
self.list_board = Board.objects.get(user=self.user).listboard_set.first()
def test_serializer_create(self):
data = {
@ -23,6 +27,7 @@ class TaskUpdateSerializerTest(TestCase):
'updated': self.end_time,
'start_datetime' : self.current_time,
'end_datetie': self.end_time,
'list_board': self.list_board.id,
}
serializer = TodoUpdateSerializer(data=data, user=self.user)
@ -32,7 +37,7 @@ class TaskUpdateSerializerTest(TestCase):
self.assertIsInstance(task, Todo)
def test_serializer_update(self):
task = Todo.objects.create(title='Original Task', notes='Original description', user=self.user)
task = Todo.objects.create(title='Original Task', notes='Original description', user=self.user, list_board=self.list_board)
data = {
'id': '32141cwaNcapufh8jq2conw',
@ -42,6 +47,7 @@ class TaskUpdateSerializerTest(TestCase):
'updated': self.end_time,
'start_datetime' : self.current_time,
'end_datetie': self.end_time,
'list_board': self.list_board.id,
}
serializer = TodoUpdateSerializer(instance=task, data=data)

View File

@ -2,72 +2,72 @@ from datetime import datetime, timedelta
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from tasks.tests.utils import create_test_user, login_user
from tasks.tests.utils import create_test_user
from tasks.models import Todo
from boards.models import ListBoard, Board
# class TodoViewSetTests(APITestCase):
# def setUp(self):
# self.user = create_test_user()
# self.client = login_user(self.user)
# self.url = reverse("todo-list")
# self.due_date = datetime.now() + timedelta(days=5)
class TodoViewSetTests(APITestCase):
def setUp(self):
self.user = create_test_user()
self.client.force_authenticate(user=self.user)
self.url = reverse("todo-list")
self.due_date = datetime.now() + timedelta(days=5)
self.list_board = Board.objects.get(user=self.user).listboard_set.first()
# def test_create_valid_todo(self):
# """
# Test creating a valid task using the API.
# """
# data = {
# 'title': 'Test Task',
# 'type': 'habit',
# 'exp': 10,
# 'attribute': 'str',
# 'priority': 1,
# 'difficulty': 1,
# 'user': self.user.id,
# 'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'),
# }
# response = self.client.post(self.url, data, format='json')
# self.assertEqual(response.status_code, status.HTTP_201_CREATED)
# self.assertEqual(Todo.objects.count(), 1)
# self.assertEqual(Todo.objects.get().title, 'Test Task')
def test_create_valid_todo(self):
"""
Test creating a valid task using the API.
"""
data = {
'title': 'Test Task',
'type': 'habit',
'difficulty': 1,
'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'),
'list_board': self.list_board.id,
}
response = self.client.post(self.url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Todo.objects.count(), 1)
self.assertEqual(Todo.objects.get().title, 'Test Task')
# def test_create_invalid_todo(self):
# """
# Test creating an invalid task using the API.
# """
# data = {
# 'type': 'invalid', # Invalid task type
# }
# response = self.client.post(self.url, data, format='json')
# self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
# self.assertEqual(Todo.objects.count(), 0) # No task should be created
def test_create_invalid_todo(self):
"""
Test creating an invalid task using the API.
"""
data = {
'type': 'invalid', # Invalid task type
}
response = self.client.post(self.url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
self.assertEqual(Todo.objects.count(), 0) # No task should be created
# def test_missing_required_fields(self):
# """
# Test creating a task with missing required fields using the API.
# """
# data = {
# 'title': 'Incomplete Task',
# 'type': 'habit',
# }
# response = self.client.post(self.url, data, format='json')
# self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
# self.assertEqual(Todo.objects.count(), 0) # No task should be created
def test_missing_required_fields(self):
"""
Test creating a task with missing required fields using the API.
"""
data = {
'type': 'habit',
}
response = self.client.post(self.url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
self.assertEqual(Todo.objects.count(), 0) # No task should be created
# def test_invalid_user_id(self):
# """
# Test creating a task with an invalid user ID using the API.
# """
# data = {
# 'title': 'Test Task',
# 'type': 'habit',
# 'exp': 10,
# 'priority': 1,
# 'difficulty': 1,
# 'user': 999, # Invalid user ID
# 'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'),
# }
# response = self.client.post(self.url, data, format='json')
# self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
# self.assertEqual(Todo.objects.count(), 0) # No task should be created
def test_invalid_user_id(self):
"""
Test creating a task with an invalid user ID using the API (OK because we retreive)
id from request.
"""
data = {
'title': 'Test Task',
'type': 'habit',
'exp': 10,
'priority': 1,
'difficulty': 1,
'user': -100, # Invalid user ID
'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'),
'list_board': self.list_board.id,
}
response = self.client.post(self.url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Todo.objects.count(), 1) # No task should be created

View File

@ -1,36 +1,39 @@
from datetime import datetime, timedelta, timezone
from django.test import TestCase
from rest_framework.test import APITestCase
from tasks.models import Todo
from tasks.tests.utils import create_test_user
from boards.models import Board
class TodoPriorityTest(TestCase):
class TodoPriorityTest(APITestCase):
def setUp(self):
self.user = create_test_user()
self.client.force_authenticate(user=self.user)
self.list_board = Board.objects.get(user=self.user).listboard_set.first()
def test_priority_calculation(self):
# Important = 2, Till Due = none
todo = Todo(importance=2, end_event=None, user=self.user)
todo = Todo(importance=2, end_event=None, user=self.user, list_board=self.list_board)
todo.save()
# 'Not Important & Not Urgent'
self.assertEqual(todo.priority, Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT)
due_date = datetime.now(timezone.utc) + timedelta(days=1)
# Important = 4, Till Due = 1
todo = Todo(importance=4, end_event=due_date, user=self.user)
todo = Todo(importance=4, end_event=due_date, user=self.user, list_board=self.list_board)
todo.save()
# 'Important & Urgent'
self.assertEqual(todo.priority, Todo.EisenhowerMatrix.IMPORTANT_URGENT)
due_date = datetime.now(timezone.utc) + timedelta(days=10)
# Important = 3, Till Due = 10
todo = Todo(importance=3, end_event=due_date, user=self.user)
todo = Todo(importance=3, end_event=due_date, user=self.user, list_board=self.list_board)
todo.save()
# 'Important & Not Urgent'
self.assertEqual(todo.priority, Todo.EisenhowerMatrix.IMPORTANT_NOT_URGENT)
due_date = datetime.now(timezone.utc) + timedelta(days=2)
# Important = 1, Till Due = 2
todo = Todo(importance=1, end_event=due_date, user=self.user)
todo = Todo(importance=1, end_event=due_date, user=self.user, list_board=self.list_board)
todo.save()
# 'Not Important & Urgent'
self.assertEqual(todo.priority, Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT)

View File

@ -1,26 +1,24 @@
from rest_framework import status
from rest_framework.test import APIClient
from django.urls import reverse
from users.models import CustomUser
from ..models import Todo
def create_test_user(email="testusertestuser@example.com", username="testusertestuser",
first_name="Test", password="testpassword",):
"""create predifined user for testing"""
return CustomUser.objects.create_user(
email=email,
username=username,
first_name=first_name,
password=password,
)
def login_user(user):
"""Login a user to API client."""
def create_test_user(email="testusertestuser@example.com",
username="testusertestuser",
password="testpassword",) -> CustomUser:
"""create predifined user without placeholder task for testing"""
client = APIClient()
client.force_authenticate(user=user)
return client
response = client.post(reverse('create_user'), {'email': email,
'username': username,
'password': password})
if response.status_code == status.HTTP_201_CREATED:
user = CustomUser.objects.get(username='testusertestuser')
user.todo_set.all().delete()
return user
return None
def create_task_json(user, **kwargs):
@ -29,10 +27,7 @@ def create_task_json(user, **kwargs):
"title": "Test Task",
"type": "habit",
"notes": "This is a test task created via the API.",
"exp": 10,
"priority": 1.5,
"difficulty": 1,
"attribute": "str",
"challenge": False,
"fromSystem": False,
"creation_date": None,
@ -51,8 +46,6 @@ def create_test_task(user, **kwargs):
'title': "Test Task",
'task_type': 'habit',
'notes': "This is a test task created via the API.",
'exp': 10,
'priority': 1.5,
'difficulty': 1,
'attribute': 'str',
'challenge': False,

View File

@ -1,9 +1,74 @@
from django.utils import timezone
from django.db.models.signals import post_save
from django.dispatch import receiver
from tasks.models import Todo
from users.models import CustomUser, UserStats
from boards.models import ListBoard, Board
@receiver(post_save, sender=CustomUser)
def create_user_stats(sender, instance, created, **kwargs):
if created:
UserStats.objects.create(user=instance)
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

@ -42,6 +42,7 @@
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-bootstrap": "^2.9.1",
"react-datepicker": "^4.23.0",
"react-datetime-picker": "^5.5.3",
"react-dom": "^18.2.0",
"react-icons": "^4.11.0",

View File

@ -101,6 +101,9 @@ dependencies:
react-bootstrap:
specifier: ^2.9.1
version: 2.9.1(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0)
react-datepicker:
specifier: ^4.23.0
version: 4.23.0(react-dom@18.2.0)(react@18.2.0)
react-datetime-picker:
specifier: ^5.5.3
version: 5.5.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0)
@ -3473,6 +3476,22 @@ packages:
- '@types/react-dom'
dev: false
/react-datepicker@4.23.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-w+msqlOZ14v6H1UknTKtZw/dw9naFMgAOspf59eY130gWpvy5dvKj/bgsFICDdvxB7PtKWxDcbGlAqCloY1d2A==}
peerDependencies:
react: ^16.9.0 || ^17 || ^18
react-dom: ^16.9.0 || ^17 || ^18
dependencies:
'@popperjs/core': 2.11.8
classnames: 2.3.2
date-fns: 2.30.0
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-onclickoutside: 6.13.0(react-dom@18.2.0)(react@18.2.0)
react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0)
dev: false
/react-datetime-picker@5.5.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-bWGEPwGrZjaXTB8P4pbTSDygctLaqTWp0nNibaz8po+l4eTh9gv3yiJ+n4NIcpIJDqZaQJO57Bnij2rAFVQyLw==}
peerDependencies:
@ -3520,6 +3539,10 @@ packages:
scheduler: 0.23.0
dev: false
/react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
dev: false
/react-fit@1.7.1(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-y/TYovCCBzfIwRJsbLj0rH4Es40wPQhU5GPPq9GlbdF09b0OdzTdMSkBza0QixSlgFzTm6dkM7oTFzaVvaBx+w==}
peerDependencies:
@ -3565,6 +3588,30 @@ packages:
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
dev: false
/react-onclickoutside@6.13.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==}
peerDependencies:
react: ^15.5.x || ^16.x || ^17.x || ^18.x
react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==}
peerDependencies:
'@popperjs/core': ^2.0.0
react: ^16.8.0 || ^17 || ^18
react-dom: ^16.8.0 || ^17 || ^18
dependencies:
'@popperjs/core': 2.11.8
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-fast-compare: 3.2.2
warning: 4.0.3
dev: false
/react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==}
peerDependencies:

View File

@ -11,7 +11,7 @@ import { SideNav } from "./components/navigations/IconSideNav";
import { Eisenhower } from "./components/EisenhowerMatrix/Eisenhower";
import { PrivateRoute } from "./PrivateRoute";
import { ProfileUpdatePage } from "./components/profile/profilePage";
import { Dashboard } from "./components/dashboard/Dashboard";
import { Dashboard } from "./components/dashboard/dashboard";
import { LandingPage } from "./components/landingPage/LandingPage";
import { PublicRoute } from "./PublicRoute";
import { useAuth } from "./hooks/AuthHooks";

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 { axiosInstance } from "src/api/AxiosConfig";
@ -46,10 +53,16 @@ export function KpiCard() {
<div>
<Metric>{kpiCardData.completedThisWeek}</Metric>
</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 className="mt-4">
<Text className="truncate">vs. {kpiCardData.completedLastWeek} (last week)</Text>
<Text className="truncate">
vs. {kpiCardData.completedLastWeek} (last week)
</Text>
</Flex>
<ProgressBar value={kpiCardData.percentage} className="mt-2" />
</Card>

View File

@ -13,7 +13,11 @@ export function DonutChartGraph() {
const completedTask = response.data.total_completed_tasks || 0;
const donutData = [
<<<<<<< HEAD
{ name: "Completed task", count: completedTask },
=======
{ name: "Completed task", count: completedTask},
>>>>>>> 4a3f253e3049f97ef4479dd423642897a56e13fc
{ name: "Total task", count: totalTask },
];

View File

@ -33,9 +33,18 @@ export function ProgressCircleChart() {
return (
<Card className="max-w-lg mx-auto">
<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">
{progressData.toFixed(0)} %
{isNaN(progressData) || !isFinite(progressData)
? "0%"
: `${progressData.toFixed(0)}%`}
</span>
</ProgressCircle>
</Flex>

View File

@ -154,7 +154,9 @@ export function Dashboard() {
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">
{progressData.toFixed(0)} %
{isNaN(progressData) || !isFinite(progressData)
? "0%"
: `${progressData.toFixed(0)}%`}
</span>
</ProgressCircle>
<br></br>

View File

@ -1,6 +1,12 @@
import { useMemo, useState, useEffect } from "react";
import { ColumnContainerCard } from "./columnContainerWrapper";
import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import {
DndContext,
DragOverlay,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import { SortableContext, arrayMove } from "@dnd-kit/sortable";
import { createPortal } from "react-dom";
import { TaskCard } from "./taskCard";
@ -26,7 +32,9 @@ export function KanbanBoard() {
// ---------------- Task Handlers ----------------
const handleTaskUpdate = (tasks, updatedTask) => {
const updatedTasks = tasks.map((task) => (task.id === updatedTask.id ? updatedTask : task));
const updatedTasks = tasks.map((task) =>
task.id === updatedTask.id ? updatedTask : task
);
setTasks(updatedTasks);
};
@ -168,8 +176,14 @@ export function KanbanBoard() {
justify-center
overflow-x-auto
overflow-y-hidden
">
<DndContext sensors={sensors} onDragStart={onDragStart} onDragEnd={onDragEnd} onDragOver={onDragOver}>
"
>
<DndContext
sensors={sensors}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onDragOver={onDragOver}
>
<div className="flex gap-4">
<div className="flex gap-4">
{!isLoading ? (
@ -181,7 +195,9 @@ export function KanbanBoard() {
createTask={createTask}
deleteTask={deleteTask}
updateTask={updateTask}
tasks={(tasks || []).filter((task) => task.columnId === col.id)}
tasks={(tasks || []).filter(
(task) => task.columnId === col.id
)}
/>
))}{" "}
</SortableContext>
@ -194,7 +210,11 @@ export function KanbanBoard() {
{createPortal(
<DragOverlay className="bg-white" dropAnimation={null} zIndex={20}>
{/* Render the active task as a draggable overlay */}
<TaskCard task={activeTask} deleteTask={deleteTask} updateTask={updateTask} />
<TaskCard
task={activeTask}
deleteTask={deleteTask}
updateTask={updateTask}
/>
</DragOverlay>,
document.body
)}
@ -302,7 +322,11 @@ export function KanbanBoard() {
const isOverAColumn = over.data.current?.type === "Column";
// Move the Task to a different column and update columnId
if (isActiveATask && isOverAColumn && tasks.some((task) => task.columnId !== overId)) {
if (
isActiveATask &&
isOverAColumn &&
tasks.some((task) => task.columnId !== overId)
) {
setTasks((tasks) => {
const activeIndex = tasks.findIndex((t) => t.id === activeId);
axiosInstance

View File

@ -1,12 +1,14 @@
import { useState } from "react";
import { BsFillTrashFill } from "react-icons/bs";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { TaskDetailModal } from "./taskDetailModal";
import { GoChecklist, GoArchive } from "react-icons/go";
export function TaskCard({ task, deleteTask, updateTask }) {
// State to track if the mouse is over the task card
const [mouseIsOver, setMouseIsOver] = useState(false);
// DnD Kit hook for sortable items
const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
id: task.id,
data: {
@ -15,50 +17,129 @@ export function TaskCard({ task, deleteTask, updateTask }) {
},
});
// Style for the task card, adjusting for dragging animation
const style = {
transition,
transform: CSS.Transform.toString(transform),
};
{
/* If card is dragged */
}
// ---- DESC AND TAG ---- */
// 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={`text-xs inline-flex items-center font-bold leading-sm uppercase px-2 py-1 bg-${tag.color}-200 text-${tag.color}-700 rounded-full`}>
{tag.label}
</div>
))}
</div>
) : null;
// difficulty?
const difficultyTag = task.difficulty ? (
<span
className={`text-[10px] 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-purple-200 text-purple-700"
: "bg-red-200 text-red-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="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) {
return (
<div
ref={setNodeRef}
style={style}
className="
opacity-30
bg-mainBackgroundColor p-2.5 items-center flex text-left rounded-xl border-2 border-gray-400 cursor-grab relative
"
className="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 (
<div>
{/* Task Detail Modal */}
<TaskDetailModal
taskId={task.id}
title={task.content}
description={task.description}
tags={task.tags}
difficulty={task.difficulty}
f challenge={task.challenge}
challenge={task.challenge}
importance={task.importance}
updateTask={updateTask}
/>
{/* -------- Task Card -------- */}
<div
ref={setNodeRef}
{...attributes}
{...listeners}
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={() => {
setMouseIsOver(true);
}}
onMouseLeave={() => {
setMouseIsOver(false);
}}>
<<<<<<< HEAD
<p
className={`p-2.5 my-auto w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-xl shadow bg-white`}
onClick={() => document.getElementById(`task_detail_modal_${task.id}`).showModal()}>
@ -74,6 +155,35 @@ export function TaskCard({ task, deleteTask, updateTask }) {
<BsFillTrashFill />
</button>
)}
=======
{/* -------- Task Content -------- */}
{/* Tags */}
{tags}
<div>
{/* Title */}
<p
className={`p-2.5 my-auto w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-xl bg-white font-semibold`}
onClick={() => document.getElementById(`task_detail_modal_${task.id}`).showModal()}>
{task.content}
</p>
{/* -------- Archive Task Button -------- */}
{mouseIsOver && (
<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>
>>>>>>> 4a3f253e3049f97ef4479dd423642897a56e13fc
</div>
</div>
);

View File

@ -2,11 +2,19 @@ import { useState } from "react";
import { FaTasks, FaRegListAlt } from "react-icons/fa";
import { FaPlus } from "react-icons/fa6";
import { TbChecklist } from "react-icons/tb";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId }) {
export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId, updateTask }) {
const [isChallengeChecked, setChallengeChecked] = useState(challenge);
const [isImportantChecked, setImportantChecked] = useState(importance);
const [currentDifficulty, setCurrentDifficulty] = useState(difficulty);
const [selectedTags, setSelectedTags] = useState([]);
const [dateStart, setDateStart] = useState(new Date());
const [dateEnd, setDateEnd] = useState(new Date());
const [startDateEnabled, setStartDateEnabled] = useState(false);
const [endDateEnabled, setEndDateEnabled] = useState(false);
const [isTaskComplete, setTaskComplete] = useState(false);
const handleChallengeChange = () => {
setChallengeChecked(!isChallengeChecked);
@ -20,6 +28,51 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
setCurrentDifficulty(parseInt(event.target.value, 10));
};
const handleTagChange = (tag) => {
const isSelected = selectedTags.includes(tag);
setSelectedTags(isSelected ? selectedTags.filter((selectedTag) => selectedTag !== tag) : [...selectedTags, tag]);
};
const handleStartDateChange = () => {
if (!isTaskComplete) {
setStartDateEnabled(!startDateEnabled);
}
};
const handleEndDateChange = () => {
if (!isTaskComplete) {
setEndDateEnabled(!endDateEnabled);
}
};
const handleTaskCompleteChange = () => {
if (isTaskComplete) {
setTaskComplete(false);
} else {
setTaskComplete(true);
setStartDateEnabled(false);
setEndDateEnabled(false);
}
};
// Existing tags
const existingTags = tags.map((tag, index) => (
<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.label}
</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.label}
</div>
));
return (
<dialog id={`task_detail_modal_${taskId}`} className="modal">
<div className="modal-box w-4/5 max-w-3xl">
@ -35,7 +88,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
<p className="text-xs">{title}</p>
</div>
</div>
{/* Tags */}
<div className="flex flex-col py-2 pb-4">
<div className="flex flex-row space-x-5">
@ -43,19 +95,81 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
<label tabIndex={0} className="btn-md border-2 rounded-xl m-1 py-1">
+ Add Tags
</label>
<ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
<li>
<a>
<input type="checkbox" checked="checked" className="checkbox checkbox-sm" />
Item 2
</a>
</li>
<ul tabIndex={0} className="dropdown-content z-[10] menu p-2 shadow bg-base-100 rounded-box w-52">
{tags.map((tag, index) => (
<li key={index}>
<label className="cursor-pointer space-x-2">
<input
type="checkbox"
checked={selectedTags.includes(tag)}
className="checkbox checkbox-sm"
onChange={() => handleTagChange(tag)}
/>
{tag.label}
</label>
</li>
))}
</ul>
</div>
</div>
<div className="flex flex-nowrap overflow-x-auto"></div>
<div className="flex flex-nowrap overflow-x-auto">
{existingTags}
{selectedTagElements}
</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"
onChange={handleStartDateChange}
/>
<div className={`rounded p-2 shadow border-2 ${!startDateEnabled && "opacity-50"}`}>
<DatePicker
selected={dateStart}
onChange={(date) => setDateStart(date)}
disabled={!startDateEnabled}
/>
</div>
</div>
</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"
onChange={handleTaskCompleteChange}
/>
</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"
onChange={handleEndDateChange}
/>
<div className={`rounded p-2 shadow border-2 ${!endDateEnabled && "opacity-50"}`}>
<DatePicker selected={dateEnd} onChange={(date) => setDateEnd(date)} disabled={!endDateEnabled} />
</div>
</div>
</div>
</div>
{/* Description */}
<div className="flex flex-col gap-2">
<h2 className="font-bold">
@ -68,7 +182,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
{description}
</textarea>
</div>
{/* Difficulty, Challenge, and Importance */}
<div className="flex flex-row space-x-3 my-4">
<div className="flex-1 card shadow border-2 p-2">
@ -120,7 +233,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
</div>
</div>
</div>
{/* Subtask */}
<div className="flex flex-col pt-2">
<h2 className="font-bold">
@ -137,7 +249,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
</button>
</div>
</div>
<form method="dialog">
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">X</button>
</form>