Merge pull request #53 from TurTaskProject/feature/tasks-api

Connect Api with Kanban
This commit is contained in:
Sirin Puenggun 2023-11-21 11:31:19 +07:00 committed by GitHub
commit 9dd99bcd42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 592 additions and 245 deletions

View File

@ -1,5 +1,5 @@
from django.contrib import admin from django.contrib import admin
from .models import Board, ListBoard from .models import Board, ListBoard, KanbanTaskOrder
@admin.register(Board) @admin.register(Board)
class BoardAdmin(admin.ModelAdmin): class BoardAdmin(admin.ModelAdmin):
@ -9,3 +9,9 @@ class BoardAdmin(admin.ModelAdmin):
class ListBoardAdmin(admin.ModelAdmin): class ListBoardAdmin(admin.ModelAdmin):
list_display = ['name', 'position', 'board'] list_display = ['name', 'position', 'board']
list_filter = ['board', 'position'] list_filter = ['board', 'position']
@admin.register(KanbanTaskOrder)
class KanbanTaskOrderAdmin(admin.ModelAdmin):
list_display = ['list_board', 'todo_order']
list_filter = ['list_board']

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.6 on 2023-11-20 18:24
import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('boards', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='KanbanTaskOrder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('todo_order', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), blank=True, default=list, size=None)),
('list_board', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='boards.listboard')),
],
),
]

View File

@ -1,3 +1,4 @@
from django.contrib.postgres.fields import ArrayField
from django.db import models from django.db import models
from users.models import CustomUser from users.models import CustomUser
@ -18,6 +19,20 @@ class Board(models.Model):
return f"{self.name}" return f"{self.name}"
class KanbanTaskOrder(models.Model):
"""
Model to store the order of Todo tasks in a Kanban board.
:param list_board: The list board that the order belongs to.
:param todo_order: ArrayField to store the order of Todo IDs.
"""
list_board = models.OneToOneField('ListBoard', on_delete=models.CASCADE)
todo_order = ArrayField(models.PositiveIntegerField(), blank=True, default=list)
def __str__(self):
return f"Order for {self.list_board}"
class ListBoard(models.Model): class ListBoard(models.Model):
""" """
List inside a Kanban board. List inside a Kanban board.
@ -30,5 +45,12 @@ class ListBoard(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
position = models.IntegerField() position = models.IntegerField()
def save(self, *args, **kwargs):
super(ListBoard, self).save(*args, **kwargs)
kanban_order, created = KanbanTaskOrder.objects.get_or_create(list_board=self)
if not created:
return
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.name}" return f"{self.name}"

View File

@ -0,0 +1,13 @@
from rest_framework import serializers
from boards.models import Board, ListBoard
class BoardSerializer(serializers.ModelSerializer):
class Meta:
model = Board
fields = '__all__'
class ListBoardSerializer(serializers.ModelSerializer):
class Meta:
model = ListBoard
fields = '__all__'

View File

@ -6,6 +6,7 @@ from users.models import CustomUser
@receiver(post_save, sender=CustomUser) @receiver(post_save, sender=CustomUser)
def create_default_board(sender, instance, created, **kwargs): def create_default_board(sender, instance, created, **kwargs):
"""Signal handler to automatically create a default Board for a user upon creation."""
if created: if created:
board = Board.objects.create(user=instance, name="My Default Board") board = Board.objects.create(user=instance, name="My Default Board")

View File

@ -1,5 +1,11 @@
from django.urls import path from django.urls import path, include
from rest_framework.routers import DefaultRouter
from boards.views import BoardViewSet, ListBoardViewSet
router = DefaultRouter()
router.register(r'boards', BoardViewSet, basename='board')
router.register(r'lists', ListBoardViewSet, basename='listboard')
urlpatterns = [ urlpatterns = [
path('', include(router.urls)),
] ]

View File

@ -1,3 +1,31 @@
from django.shortcuts import render from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status
from boards.models import Board, ListBoard
from boards.serializers import BoardSerializer, ListBoardSerializer
class BoardViewSet(viewsets.ModelViewSet):
queryset = Board.objects.all()
serializer_class = BoardSerializer
http_method_names = ['get']
def get_queryset(self):
queryset = Board.objects.filter(user_id=self.request.user.id)
return queryset
class ListBoardViewSet(viewsets.ModelViewSet):
serializer_class = ListBoardSerializer
def get_queryset(self):
queryset = ListBoard.objects.filter(board__user_id=self.request.user.id)
return queryset
def create(self, request, *args, **kwargs):
board_id = request.data.get('board')
board = Board.objects.get(id=board_id)
if request.user.id != board.user.id:
return Response({"error": "Cannot create ListBoard for another user's board."}, status=status.HTTP_403_FORBIDDEN)
return super().create(request, *args, **kwargs)
# Create your views here.

View File

@ -2,12 +2,13 @@ from django.db.models.signals import pre_save, post_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 from boards.models import ListBoard, Board
from tasks.models import Todo from tasks.models import Todo
@receiver(pre_save, sender=Todo) @receiver(pre_save, sender=Todo)
def update_priority(sender, instance, **kwargs): def update_priority(sender, instance, **kwargs):
"""Update the priority of a Todo based on the Eisenhower Matrix"""
if instance.end_event: if instance.end_event:
time_until_due = (instance.end_event - timezone.now()).days time_until_due = (instance.end_event - timezone.now()).days
else: else:
@ -28,6 +29,7 @@ def update_priority(sender, instance, **kwargs):
@receiver(post_save, sender=Todo) @receiver(post_save, sender=Todo)
def assign_todo_to_listboard(sender, instance, created, **kwargs): 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: if created:
user_board = instance.user.board_set.first() user_board = instance.user.board_set.first()

View File

@ -1,5 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from ..models import Todo, RecurrenceTask, Habit from boards.models import ListBoard
from tasks.models import Todo, RecurrenceTask, Habit
class TaskSerializer(serializers.ModelSerializer): class TaskSerializer(serializers.ModelSerializer):
class Meta: class Meta:
@ -13,8 +14,52 @@ class TaskSerializer(serializers.ModelSerializer):
class TaskCreateSerializer(serializers.ModelSerializer): class TaskCreateSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Todo model = Todo
exclude = ('tags',) exclude = ('tags', 'google_calendar_id', 'creation_date', 'last_update',)
class ChangeTaskOrderSerializer(serializers.Serializer):
list_board_id = serializers.IntegerField(
help_text='ID of the ListBoard for which the task order should be updated.'
)
todo_order = serializers.ListField(
child=serializers.IntegerField(),
required=False,
help_text='New order of Todo IDs in the ListBoard.'
)
def validate(self, data):
list_board_id = data.get('list_board_id')
todo_order = data.get('todo_order', [])
if not ListBoard.objects.filter(id=list_board_id).exists():
raise serializers.ValidationError('ListBoard does not exist.')
existing_tasks = Todo.objects.filter(id__in=todo_order)
existing_task_ids = set(task.id for task in existing_tasks)
non_existing_task_ids = set(todo_order) - existing_task_ids
if non_existing_task_ids:
raise serializers.ValidationError(f'Tasks with IDs {non_existing_task_ids} do not exist.')
return data
class ChangeTaskListBoardSerializer(serializers.Serializer):
todo_id = serializers.IntegerField()
new_list_board_id = serializers.IntegerField()
new_index = serializers.IntegerField(required=False)
def validate(self, data):
todo_id = data.get('todo_id')
new_list_board_id = data.get('new_list_board_id')
new_index = data.get('new_index')
if not Todo.objects.filter(id=todo_id, user=self.context['request'].user).exists():
raise serializers.ValidationError('Todo does not exist for the authenticated user.')
if not ListBoard.objects.filter(id=new_list_board_id).exists():
raise serializers.ValidationError('ListBoard does not exist.')
return data
class RecurrenceTaskSerializer(serializers.ModelSerializer): class RecurrenceTaskSerializer(serializers.ModelSerializer):
class Meta: class Meta:

View File

@ -1,5 +1,12 @@
from rest_framework import viewsets from django.shortcuts import get_object_or_404
from django.db import IntegrityError
from rest_framework import viewsets, status, serializers
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 .serializers import ChangeTaskListBoardSerializer, ChangeTaskOrderSerializer
from boards.models import ListBoard, KanbanTaskOrder
from tasks.models import Todo, RecurrenceTask, Habit from tasks.models import Todo, RecurrenceTask, Habit
from tasks.tasks.serializers import (TaskCreateSerializer, from tasks.tasks.serializers import (TaskCreateSerializer,
TaskSerializer, TaskSerializer,
@ -13,6 +20,7 @@ class TodoViewSet(viewsets.ModelViewSet):
queryset = Todo.objects.all() queryset = Todo.objects.all()
serializer_class = TaskSerializer serializer_class = TaskSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
model = Todo
def get_queryset(self): def get_queryset(self):
queryset = Todo.objects.filter(user=self.request.user) queryset = Todo.objects.filter(user=self.request.user)
@ -24,6 +32,90 @@ class TodoViewSet(viewsets.ModelViewSet):
return TaskCreateSerializer return TaskCreateSerializer
return TaskSerializer return TaskSerializer
def create(self, request, *args, **kwargs):
try:
new_task_data = request.data
new_task_data['user'] = self.request.user.id
serializer = self.get_serializer(data=new_task_data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
except IntegrityError as e:
return Response({'error': 'IntegrityError - Duplicate Entry'}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['put'])
def change_task_order(self, request):
try:
serializer = ChangeTaskOrderSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
list_board_id = serializer.validated_data['list_board_id']
new_order = serializer.validated_data.get('todo_order', [])
list_board = get_object_or_404(ListBoard, id=list_board_id)
kanban_order, created = KanbanTaskOrder.objects.get_or_create(list_board=list_board)
kanban_order.todo_order = new_order
kanban_order.save()
return Response({'message': 'Task order updated successfully'})
except serializers.ValidationError as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['put'])
def change_task_list_board(self, request):
try:
serializer = ChangeTaskListBoardSerializer(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
todo_id = serializer.validated_data['todo_id']
new_list_board_id = serializer.validated_data['new_list_board_id']
new_index = serializer.validated_data.get('new_index')
todo_id = request.data.get('todo_id')
new_list_board_id = request.data.get('new_list_board_id')
todo = get_object_or_404(Todo, id=todo_id, user=self.request.user)
old_list_board = todo.list_board
# Remove todoId from todo_order of the old list board
old_kanban_order, _ = KanbanTaskOrder.objects.get_or_create(list_board=old_list_board)
old_kanban_order.todo_order = [t_id for t_id in old_kanban_order.todo_order if t_id != todo.id]
old_kanban_order.save()
# Get the index to insert the todo in the new list board's todo_order
new_list_board = get_object_or_404(ListBoard, id=new_list_board_id)
new_kanban_order, _ = KanbanTaskOrder.objects.get_or_create(list_board=new_list_board)
# Index where todo need to insert (start from 0)
new_index = request.data.get('new_index', None)
if new_index is not None and 0 <= new_index <= len(new_kanban_order.todo_order):
new_kanban_order.todo_order.insert(new_index, todo.id)
else:
new_kanban_order.todo_order.append(todo.id)
new_kanban_order.save()
todo.list_board = new_list_board
todo.save()
return Response({'message': 'ListBoard updated successfully'})
except serializers.ValidationError as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
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()

View File

@ -6,68 +6,68 @@ from tasks.tests.utils import create_test_user, login_user
from tasks.models import Todo from tasks.models import Todo
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 = login_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)
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, # 'exp': 10,
'attribute': 'str', # 'attribute': 'str',
'priority': 1, # 'priority': 1,
'difficulty': 1, # 'difficulty': 1,
'user': self.user.id, # 'user': self.user.id,
'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'), # 'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'),
} # }
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_201_CREATED) # self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Todo.objects.count(), 1) # self.assertEqual(Todo.objects.count(), 1)
self.assertEqual(Todo.objects.get().title, 'Test Task') # 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_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_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', # '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_400_BAD_REQUEST) # 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.
""" # """
data = { # data = {
'title': 'Test Task', # 'title': 'Test Task',
'type': 'habit', # 'type': 'habit',
'exp': 10, # 'exp': 10,
'priority': 1, # 'priority': 1,
'difficulty': 1, # 'difficulty': 1,
'user': 999, # Invalid user ID # 'user': 999, # Invalid user ID
'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'), # 'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'),
} # }
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_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

View File

@ -43,7 +43,8 @@ export default class Calendar extends React.Component {
renderSidebar() { renderSidebar() {
return ( return (
<div className="w-72 bg-blue-100 border-r border-blue-200 p-8 flex-shrink-0"> <div className="w-72 bg-blue-100 border-r border-blue-200 p-8 flex flex-col">
{/* Description Zone */}
<div className="mb-8"> <div className="mb-8">
<h2 className="text-xl font-bold">Instructions</h2> <h2 className="text-xl font-bold">Instructions</h2>
<ul className="list-disc pl-4"> <ul className="list-disc pl-4">
@ -53,6 +54,7 @@ export default class Calendar extends React.Component {
</ul> </ul>
</div> </div>
{/* Toggle */}
<div className="mb-8"> <div className="mb-8">
<label className="flex items-center"> <label className="flex items-center">
<input <input
@ -65,7 +67,8 @@ export default class Calendar extends React.Component {
</label> </label>
</div> </div>
<div> {/* Show all task */}
<div className="overflow-y-auto">
<h2 className="text-xl font-bold">All Events ({this.state.currentEvents.length})</h2> <h2 className="text-xl font-bold">All Events ({this.state.currentEvents.length})</h2>
<ul>{this.state.currentEvents.map(renderSidebarEvent)}</ul> <ul>{this.state.currentEvents.map(renderSidebarEvent)}</ul>
</div> </div>

View File

@ -1,100 +1,18 @@
import { useMemo, useState } from "react"; import { useMemo, useState, useEffect } from "react";
import ColumnContainerCard from "./columnContainerWrapper"; 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 { SortableContext, arrayMove } from "@dnd-kit/sortable";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import TaskCard from "./taskCard"; import TaskCard from "./taskCard";
import { AiOutlinePlusCircle } from "react-icons/ai"; import { AiOutlinePlusCircle } from "react-icons/ai";
import axiosInstance from "../../api/configs/AxiosConfig";
const defaultCols = [
{
id: "todo",
title: "Todo",
},
{
id: "doing",
title: "Work in progress",
},
{
id: "done",
title: "Done",
},
];
const defaultTasks = [
{
id: "1",
columnId: "todo",
content: "List admin APIs for dashboard",
},
{
id: "2",
columnId: "todo",
content:
"Develop user registration functionality with OTP delivered on SMS after email confirmation and phone number confirmation",
},
{
id: "3",
columnId: "doing",
content: "Conduct security testing",
},
{
id: "4",
columnId: "doing",
content: "Analyze competitors",
},
{
id: "5",
columnId: "done",
content: "Create UI kit documentation",
},
{
id: "6",
columnId: "done",
content: "Dev meeting",
},
{
id: "7",
columnId: "done",
content: "Deliver dashboard prototype",
},
{
id: "8",
columnId: "todo",
content: "Optimize application performance",
},
{
id: "9",
columnId: "todo",
content: "Implement data validation",
},
{
id: "10",
columnId: "todo",
content: "Design database schema",
},
{
id: "11",
columnId: "todo",
content: "Integrate SSL web certificates into workflow",
},
{
id: "12",
columnId: "doing",
content: "Implement error logging and monitoring",
},
{
id: "13",
columnId: "doing",
content: "Design and implement responsive UI",
},
];
function KanbanBoard() { function KanbanBoard() {
const [columns, setColumns] = useState(defaultCols); const [columns, setColumns] = useState([]);
const columnsId = useMemo(() => columns.map(col => col.id), [columns]); const columnsId = useMemo(() => columns.map(col => col.id), [columns]);
const [boardId, setBoardData] = useState();
const [tasks, setTasks] = useState(defaultTasks); const [tasks, setTasks] = useState([]);
const [activeColumn, setActiveColumn] = useState(null); const [activeColumn, setActiveColumn] = useState(null);
@ -108,6 +26,101 @@ function KanbanBoard() {
}) })
); );
// Example
// {
// "id": 95,
// "title": "Test Todo",
// "notes": "Test TodoTest TodoTest Todo",
// "importance": 1,
// "difficulty": 1,
// "challenge": false,
// "fromSystem": false,
// "creation_date": "2023-11-20T19:50:16.369308Z",
// "last_update": "2023-11-20T19:50:16.369308Z",
// "is_active": true,
// "is_full_day_event": false,
// "start_event": "2023-11-20T19:49:49Z",
// "end_event": "2023-11-23T18:00:00Z",
// "google_calendar_id": null,
// "completed": true,
// "completion_date": "2023-11-20T19:50:16.369308Z",
// "priority": 3,
// "user": 1,
// "list_board": 1,
// "tags": []
// }
// ]
// [
// {
// "id": 8,
// "name": "test",
// "position": 2,
// "board": 3
// }
// ]
useEffect(() => {
const fetchData = async () => {
try {
const tasksResponse = await axiosInstance.get("/todo");
// Transform
const transformedTasks = tasksResponse.data.map(task => ({
id: task.id,
columnId: task.list_board,
content: task.title,
difficulty: task.difficulty,
notes: task.notes,
importance: task.importance,
difficulty: task.difficulty,
challenge: task.challenge,
fromSystem: task.fromSystem,
creation_date: task.creation_date,
last_update: task.last_update,
is_active: task.is_active,
is_full_day_event: task.is_full_day_event,
start_event: task.start_event,
end_event: task.end_event,
google_calendar_id: task.google_calendar_id,
completed: task.completed,
completion_date: task.completion_date,
priority: task.priority,
user: task.user,
list_board: task.list_board,
tags: task.tags,
}));
setTasks(transformedTasks);
const columnsResponse = await axiosInstance.get("/lists");
// Transform
const transformedColumns = columnsResponse.data.map(column => ({
id: column.id,
title: column.name,
}));
setColumns(transformedColumns);
} catch (error) {
console.error("Error fetching data from API:", error);
}
};
fetchData();
}, []);
useEffect(() => {
const fetchBoardData = async () => {
try {
const response = await axiosInstance.get("boards/");
if (response.data && response.data.length > 0) {
setBoardData(response.data[0]);
}
} catch (error) {
console.error("Error fetching board data:", error);
}
};
fetchBoardData();
}, []);
return ( return (
<div <div
className=" className="
@ -184,18 +197,46 @@ function KanbanBoard() {
</div> </div>
); );
function createTask(columnId) { function createTask(columnId, setTasks) {
const newTask = { const newTaskData = {
id: generateId(), title: `Task ${tasks.length + 1}`,
columnId, importance: 1,
content: `Task ${tasks.length + 1}`, difficulty: 1,
challenge: false,
fromSystem: false,
is_active: false,
is_full_day_event: false,
completed: false,
priority: 1,
list_board: columnId,
}; };
setTasks([...tasks, newTask]); axiosInstance
.post("todo/", newTaskData)
.then(response => {
const newTask = {
id: response.data.id,
columnId,
content: response.data.title,
};
})
.catch(error => {
console.error("Error creating task:", error);
});
setTasks(tasks => [...tasks, newTask]);
} }
function deleteTask(id) { function deleteTask(id) {
const newTasks = tasks.filter(task => task.id !== id); const newTasks = tasks.filter(task => task.id !== id);
axiosInstance
.delete(`todo/${id}/`)
.then(response => {
setTasks(newTasks);
})
.catch(error => {
console.error("Error deleting Task:", error);
});
setTasks(newTasks); setTasks(newTasks);
} }
@ -209,29 +250,55 @@ function KanbanBoard() {
} }
function createNewColumn() { function createNewColumn() {
const columnToAdd = { axiosInstance
id: generateId(), .post("lists/", { name: `Column ${columns.length + 1}`, position: 1, board: boardId.id })
title: `Column ${columns.length + 1}`, .then(response => {
const newColumn = {
id: response.data.id,
title: response.data.name,
}; };
setColumns([...columns, columnToAdd]); setColumns(prevColumns => [...prevColumns, newColumn]);
})
.catch(error => {
console.error("Error creating ListBoard:", error);
});
} }
function deleteColumn(id) { function deleteColumn(id) {
const filteredColumns = columns.filter(col => col.id !== id); axiosInstance
setColumns(filteredColumns); .delete(`lists/${id}/`)
.then(response => {
setColumns(prevColumns => prevColumns.filter(col => col.id !== id));
})
.catch(error => {
console.error("Error deleting ListBoard:", error);
});
const newTasks = tasks.filter(t => t.columnId !== id); const tasksToDelete = tasks.filter(t => t.columnId === id);
setTasks(newTasks);
tasksToDelete.forEach(task => {
axiosInstance
.delete(`todo/${task.id}/`)
.then(response => {
setTasks(prevTasks => prevTasks.filter(t => t.id !== task.id));
})
.catch(error => {
console.error("Error deleting Task:", error);
});
});
} }
function updateColumn(id, title) { function updateColumn(id, title) {
const newColumns = columns.map(col => { // Update the column
if (col.id !== id) return col; axiosInstance
return { ...col, title }; .patch(`lists/${id}/`, { name: title }) // Adjust the payload based on your API requirements
.then(response => {
setColumns(prevColumns => prevColumns.map(col => (col.id === id ? { ...col, title } : col)));
})
.catch(error => {
console.error("Error updating ListBoard:", error);
}); });
setColumns(newColumns);
} }
function onDragStart(event) { function onDragStart(event) {
@ -256,20 +323,54 @@ function KanbanBoard() {
const activeId = active.id; const activeId = active.id;
const overId = over.id; const overId = over.id;
if (activeId === overId) return;
const isActiveAColumn = active.data.current?.type === "Column"; const isActiveAColumn = active.data.current?.type === "Column";
if (!isActiveAColumn) return; const isActiveATask = active.data.current?.type === "Task";
const isOverAColumn = over.data.current?.type === "Column";
const isOverATask = over.data.current?.type === "Task";
// Reorder columns if the dragged item is a column
if (isActiveAColumn && isOverAColumn) {
setColumns(columns => { setColumns(columns => {
const activeColumnIndex = columns.findIndex(col => col.id === activeId); const activeColumnIndex = columns.findIndex(col => col.id === activeId);
const overColumnIndex = columns.findIndex(col => col.id === overId); const overColumnIndex = columns.findIndex(col => col.id === overId);
return arrayMove(columns, activeColumnIndex, overColumnIndex); const reorderedColumns = arrayMove(columns, activeColumnIndex, overColumnIndex);
return reorderedColumns;
}); });
} }
// Reorder tasks within the same column
if (isActiveATask && isOverATask) {
setTasks(tasks => {
const activeIndex = tasks.findIndex(t => t.id === activeId);
const overIndex = tasks.findIndex(t => t.id === overId);
const reorderedTasks = arrayMove(tasks, activeIndex, overIndex);
return reorderedTasks;
});
}
// Move tasks between columns and update columnId
if (isActiveATask && isOverAColumn) {
setTasks(tasks => {
const activeIndex = tasks.findIndex(t => t.id === activeId);
tasks[activeIndex].columnId = overId;
axiosInstance
.put(`todo/change_task_list_board/`, { todo_id: activeId, new_list_board_id: overId, new_index: 0 })
.then(response => {})
.catch(error => {
console.error("Error updating task columnId:", error);
});
return arrayMove(tasks, activeIndex, activeIndex);
});
}
}
function onDragOver(event) { function onDragOver(event) {
const { active, over } = event; const { active, over } = event;
if (!over) return; if (!over) return;

View File

@ -29,6 +29,8 @@ const KanbanPage = () => {
</div> </div>
</div> </div>
<KanbanBoard /> <KanbanBoard />
<div className="flex justify-center border-2 ">
</div>
</div> </div>
); );
}; };

View File

@ -4,7 +4,7 @@ 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";
function TaskCard({ task, deleteTask, updateTask }) { function TaskCard({ task, deleteTask, updateTask}) {
const [mouseIsOver, setMouseIsOver] = useState(false); const [mouseIsOver, setMouseIsOver] = useState(false);
const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({ const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
@ -15,6 +15,7 @@ function TaskCard({ task, deleteTask, updateTask }) {
}, },
}); });
const style = { const style = {
transition, transition,
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
@ -38,7 +39,15 @@ function TaskCard({ task, deleteTask, updateTask }) {
return ( return (
<div> <div>
<TaskDetailModal /> <TaskDetailModal
taskId={task.id}
title={task.content}
description={task.description}
tags={task.tags}
difficulty={task.difficulty}
challenge={task.challenge}
importance={task.importance}
/>
<div <div
ref={setNodeRef} ref={setNodeRef}
{...attributes} {...attributes}
@ -53,7 +62,7 @@ function TaskCard({ task, deleteTask, updateTask }) {
}}> }}>
<p <p
className="p-2.5 my-auto w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-xl shadow bg-white" 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").showModal()}> onClick={() => document.getElementById(`task_detail_modal_${task.id}`).showModal()}>
{task.content} {task.content}
</p> </p>

View File

@ -3,10 +3,10 @@ import { FaTasks, FaRegListAlt } from "react-icons/fa";
import { FaPlus } from "react-icons/fa6"; import { FaPlus } from "react-icons/fa6";
import { TbChecklist } from "react-icons/tb"; import { TbChecklist } from "react-icons/tb";
function TaskDetailModal() { function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId }) {
const [difficulty, setDifficulty] = useState(50); const [isChallengeChecked, setChallengeChecked] = useState(challenge);
const [isChallengeChecked, setChallengeChecked] = useState(true); const [isImportantChecked, setImportantChecked] = useState(importance);
const [isImportantChecked, setImportantChecked] = useState(true); const [currentDifficulty, setCurrentDifficulty] = useState(difficulty);
const handleChallengeChange = () => { const handleChallengeChange = () => {
setChallengeChecked(!isChallengeChecked); setChallengeChecked(!isChallengeChecked);
@ -15,20 +15,23 @@ function TaskDetailModal() {
const handleImportantChange = () => { const handleImportantChange = () => {
setImportantChecked(!isImportantChecked); setImportantChecked(!isImportantChecked);
}; };
const handleDifficultyChange = event => {
setDifficulty(parseInt(event.target.value, 10)); const handleDifficultyChange = (event) => {
setCurrentDifficulty(parseInt(event.target.value, 10));
}; };
return ( return (
<dialog id="task_detail_modal" 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"> <h3 className="font-bold text-lg">
<span className="flex gap-2">{<FaTasks className="my-2" />}Title</span> <span className="flex gap-2">
{<FaTasks className="my-2" />}{title}
</span>
</h3> </h3>
<p className="text-xs">Todo List</p> <p className="text-xs">{title}</p>
</div> </div>
</div> </div>
@ -42,19 +45,7 @@ function TaskDetailModal() {
<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-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
<li> <li>
<a> <a>
<input type="checkbox" checked="checked" className="checkbox checkbox-sm" /> <input type="checkbox" checked="checked" className="checkbox checkbox-sm"/>
Item 2
</a>
</li>
<li>
<a>
<input type="checkbox" checked="checked" className="checkbox checkbox-sm" />
Item 2
</a>
</li>
<li>
<a>
<input type="checkbox" checked="checked" className="checkbox checkbox-sm" />
Item 2 Item 2
</a> </a>
</li> </li>
@ -72,10 +63,12 @@ function TaskDetailModal() {
Description Description
</span> </span>
</h2> </h2>
<textarea className="textarea w-full" disabled></textarea> <textarea className="textarea w-full" disabled>
{description}
</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">
<input <input
@ -83,7 +76,7 @@ function TaskDetailModal() {
id="difficultySelector" id="difficultySelector"
min={0} min={0}
max="100" max="100"
value={difficulty} value={currentDifficulty}
className="range" className="range"
step="25" step="25"
onChange={handleDifficultyChange} onChange={handleDifficultyChange}

View File

@ -1,6 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { AiOutlineHome, AiOutlineSchedule, AiOutlineUnorderedList, AiOutlinePieChart } from "react-icons/ai"; import { AiOutlineHome, AiOutlineSchedule, AiOutlineUnorderedList, AiOutlinePieChart } from "react-icons/ai";
import { PiStepsDuotone } from "react-icons/pi"; import { PiStepsDuotone } from "react-icons/pi";
import { IoSettingsOutline } from "react-icons/io5";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
@ -8,7 +9,7 @@ const menuItems = [
{ id: 0, path: "/", icon: <AiOutlineHome /> }, { id: 0, path: "/", icon: <AiOutlineHome /> },
{ id: 1, path: "/tasks", icon: <AiOutlineUnorderedList /> }, { id: 1, path: "/tasks", icon: <AiOutlineUnorderedList /> },
{ id: 2, path: "/calendar", icon: <AiOutlineSchedule /> }, { id: 2, path: "/calendar", icon: <AiOutlineSchedule /> },
{ id: 3, path: "/analytic", icon: <AiOutlinePieChart /> }, { id: 3, path: "/settings", icon: <IoSettingsOutline /> },
{ id: 4, path: "/priority", icon: <PiStepsDuotone /> }, { id: 4, path: "/priority", icon: <PiStepsDuotone /> },
]; ];