tables & kanban tasks
This commit is contained in:
parent
ed1b06a1c2
commit
330b813563
Binary file not shown.
Binary file not shown.
@ -10,6 +10,7 @@
|
|||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Poppins:wght@600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Poppins:wght@600&display=swap" rel="stylesheet">
|
||||||
{% load static %}
|
{% load static %}
|
||||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ timestamp }}">
|
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ timestamp }}">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||||
|
|||||||
@ -11,31 +11,116 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="kanban-board-container">
|
<div class="kanban-board-container">
|
||||||
|
<div class="loader-overlay" style="display: none;">
|
||||||
|
<div class="loader"></div>
|
||||||
|
</div>
|
||||||
|
{% csrf_token %}
|
||||||
<div class="kanban-board">
|
<div class="kanban-board">
|
||||||
{% for status_value, status_display in status_choices %}
|
{% for status_value, status_display in status_choices %}
|
||||||
<div class="kanban-column">
|
<div class="kanban-column" data-status="{{ status_value }}">
|
||||||
<h2 class="h5 p-3 bg-light border-bottom">{{ status_display }}</h2>
|
<h2 class="h5 p-3 bg-light border-bottom">{{ status_display }}</h2>
|
||||||
<div class="kanban-cards p-3">
|
<div class="kanban-cards p-3">
|
||||||
{% for item in tasks_by_status|get_item:status_value %}
|
{% with tasks=tasks_by_status|get_item:status_value %}
|
||||||
<div class="card kanban-card mb-3 shadow-sm">
|
{% if tasks %}
|
||||||
<div class="card-body">
|
{% for item in tasks %}
|
||||||
<h5 class="card-title h6">{{ item.title }}</h5>
|
<div class="card kanban-card mb-3 shadow-sm" data-task-id="{{ item.id }}">
|
||||||
<p class="card-text small">{{ item.description|default:""|truncatewords:15 }}</p>
|
<div class="card-body">
|
||||||
{% if item.tags %}
|
<h5 class="card-title h6">{{ item.title }}</h5>
|
||||||
<div class="tags">
|
<p class="card-text small">{{ item.description|default:""|truncatewords:15 }}</p>
|
||||||
{% for tag in item.tags.split|slice:":3" %}
|
{% if item.tags %}
|
||||||
<span class="badge bg-secondary">{{ tag }}</span>
|
<div class="tags">
|
||||||
{% endfor %}
|
{% for tag in item.tags.split|slice:":3" %}
|
||||||
|
<span class="badge bg-secondary">{{ tag }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
</div>
|
{% else %}
|
||||||
{% empty %}
|
<div class="text-center text-muted p-3">No tasks in this stage.</div>
|
||||||
<div class="text-center text-muted p-3">No tasks in this stage.</div>
|
{% endif %}
|
||||||
{% endfor %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const columns = document.querySelectorAll('.kanban-cards');
|
||||||
|
columns.forEach(column => {
|
||||||
|
new Sortable(column, {
|
||||||
|
group: 'kanban',
|
||||||
|
animation: 150,
|
||||||
|
onStart: function (evt) {
|
||||||
|
document.querySelector('.loader-overlay').style.display = 'flex';
|
||||||
|
},
|
||||||
|
onEnd: function (evt) {
|
||||||
|
const itemEl = evt.item;
|
||||||
|
const toContainer = evt.to;
|
||||||
|
const fromContainer = evt.from;
|
||||||
|
const taskId = itemEl.dataset.taskId;
|
||||||
|
const newStatus = toContainer.closest('.kanban-column').dataset.status;
|
||||||
|
const oldIndex = evt.oldDraggableIndex;
|
||||||
|
|
||||||
|
// Get CSRF token
|
||||||
|
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]') ? document.querySelector('[name=csrfmiddlewaretoken]').value : getCookie('csrftoken');
|
||||||
|
|
||||||
|
fetch('/update_task_status/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': csrftoken
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
task_id: taskId,
|
||||||
|
new_status: newStatus
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Server responded with an error!');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
// Revert the move in the UI
|
||||||
|
fromContainer.insertBefore(itemEl, fromContainer.children[oldIndex]);
|
||||||
|
alert('Failed to update task status. Please try again.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error updating task status:', error);
|
||||||
|
// Revert the move in the UI
|
||||||
|
fromContainer.insertBefore(itemEl, fromContainer.children[oldIndex]);
|
||||||
|
alert('An error occurred while updating the task. Please try again.');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
document.querySelector('.loader-overlay').style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
let cookieValue = null;
|
||||||
|
if (document.cookie && document.cookie !== '') {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import index, article_detail, kanban_board
|
from .views import index, article_detail, kanban_board, update_task_status
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", index, name="index"),
|
path("", index, name="index"),
|
||||||
path("kanban/", kanban_board, name="kanban_board"),
|
path("kanban/", kanban_board, name="kanban_board"),
|
||||||
path("article/<int:article_id>/", article_detail, name="article_detail"),
|
path("article/<int:article_id>/", article_detail, name="article_detail"),
|
||||||
|
path('update_task_status/', update_task_status, name='update_task_status'),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
import json
|
||||||
from .models import Article, TodoItem
|
from .models import Article, TodoItem
|
||||||
from .forms import TodoItemForm
|
from .forms import TodoItemForm
|
||||||
import time
|
import time
|
||||||
@ -39,4 +42,19 @@ def kanban_board(request):
|
|||||||
|
|
||||||
def article_detail(request, article_id):
|
def article_detail(request, article_id):
|
||||||
article = Article.objects.get(pk=article_id)
|
article = Article.objects.get(pk=article_id)
|
||||||
return render(request, "core/article_detail.html", {"article": article})
|
return render(request, "core/article_detail.html", {"article": article})
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def update_task_status(request):
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
task_id = data.get('task_id')
|
||||||
|
new_status = data.get('new_status')
|
||||||
|
|
||||||
|
task = get_object_or_404(TodoItem, id=task_id)
|
||||||
|
task.status = new_status
|
||||||
|
task.save()
|
||||||
|
|
||||||
|
return JsonResponse({'success': True})
|
||||||
|
except (json.JSONDecodeError, TypeError, ValueError) as e:
|
||||||
|
return JsonResponse({'success': False, 'error': str(e)}, status=400)
|
||||||
@ -153,3 +153,30 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
.kanban-card .tags {
|
.kanban-card .tags {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loader-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
border: 8px solid #f3f3f3;
|
||||||
|
border-top: 8px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|||||||
@ -153,3 +153,30 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
.kanban-card .tags {
|
.kanban-card .tags {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loader-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
border: 8px solid #f3f3f3;
|
||||||
|
border-top: 8px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user