Files
DevSuite/templates/projects/partials/kanban_board.html
T
sankalp 036a6471a3 feat(projects/partials): add kanban board and project form templates
Introduce two new template partials for projects: a kanban board interface and a multi-step project form. These additions provide enhanced project management capabilities and streamlined project creation.

**New Features**
- **Kanban board** with four columns (To Do, In Progress, Review, Done)
- Dynamic task cards with drag-and-drop functionality
- Task CRUD operations via modals (Add Task, Edit Task)
- Status badges showing task counts per column
- **Project form** with three-step wizard interface
- Form validation and dynamic step navigation
- Responsive design for both components

**Enhancements & Refinements**
- Interactive UI elements with hover states and visual feedback
- Automatic count updates when tasks are moved or modified
- HTMX integration for seamless form submissions
- Clean and modern styling using utility classes
- Accessibility improvements with proper ARIA labels and focus management
2025-12-14 09:39:00 +05:45

423 lines
21 KiB
HTML

<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
<!-- TODO Column -->
<div class="card bg-base-200 shadow-sm">
<div class="card-body p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="font-bold text-base-content/70 uppercase text-sm tracking-wider">To Do</h3>
<span class="badge badge-sm" id="todo-count">{{ tasks_todo|length }}</span>
</div>
<!-- Add Task Button -->
<button onclick="add_task_modal.showModal()" class="btn btn-sm btn-primary w-full mb-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Add Task
</button>
<div id="kanban-todo-list"
class="column-list flex flex-col gap-3 min-h-[150px] md:min-h-[200px] bg-base-100/50 p-2 rounded-lg"
data-status="TODO">
{% for task in tasks_todo %}
<div class="task-item card bg-base-100 shadow-sm hover:shadow-md p-3 cursor-move" draggable="true"
data-task-id="{{ task.id }}" id="task-{{ task.id }}">
<div class="flex items-start justify-between gap-2">
<div class="flex-1">
<p class="text-sm font-medium">{{ task.title }}</p>
{% if task.due_date %}
<div class="text-xs text-base-content/60 mt-1 flex items-center gap-1">
<span class="icon-[tabler--calendar] size-3"></span>
{{ task.due_date|date:"M d" }}
</div>
{% endif %}
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-xs btn-square">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
</svg>
</div>
<ul tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-32">
<li><a class="edit-task-btn" data-task-id="{{ task.id }}"
data-task-title="{{ task.title }}"
data-task-due-date="{{ task.due_date|date:'Y-m-d'|default:'' }}">Edit</a></li>
<li><a class="delete-task-btn" data-task-id="{{ task.id }}">Delete</a></li>
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- IN PROGRESS Column -->
<div class="card bg-base-200 shadow-sm">
<div class="card-body p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="font-bold text-info uppercase text-sm tracking-wider">In Progress</h3>
<span class="badge badge-info badge-sm" id="progress-count">{{ tasks_in_progress|length }}</span>
</div>
<div id="kanban-progress-list"
class="column-list flex flex-col gap-3 min-h-[150px] md:min-h-[200px] bg-base-100/50 p-2 rounded-lg"
data-status="IN_PROGRESS">
{% for task in tasks_in_progress %}
<div class="task-item card bg-base-100 shadow-sm hover:shadow-md p-3 cursor-move" draggable="true"
data-task-id="{{ task.id }}" id="task-{{ task.id }}">
<div class="flex items-start justify-between gap-2">
<div class="flex-1">
<p class="text-sm font-medium">{{ task.title }}</p>
{% if task.due_date %}
<div class="text-xs text-base-content/60 mt-1 flex items-center gap-1">
<span class="icon-[tabler--calendar] size-3"></span>
{{ task.due_date|date:"M d" }}
</div>
{% endif %}
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-xs btn-square">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
</svg>
</div>
<ul tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-32">
<li><a class="edit-task-btn" data-task-id="{{ task.id }}"
data-task-title="{{ task.title }}"
data-task-due-date="{{ task.due_date|date:'Y-m-d'|default:'' }}">Edit</a></li>
<li><a class="delete-task-btn" data-task-id="{{ task.id }}">Delete</a></li>
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- REVIEW Column -->
<div class="card bg-base-200 shadow-sm">
<div class="card-body p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="font-bold text-warning uppercase text-sm tracking-wider">Review</h3>
<span class="badge badge-warning badge-sm" id="review-count">{{ tasks_review|length }}</span>
</div>
<div id="kanban-review-list"
class="column-list flex flex-col gap-3 min-h-[150px] md:min-h-[200px] bg-base-100/50 p-2 rounded-lg"
data-status="REVIEW">
{% for task in tasks_review %}
<div class="task-item card bg-base-100 shadow-sm hover:shadow-md p-3 cursor-move" draggable="true"
data-task-id="{{ task.id }}" id="task-{{ task.id }}">
<div class="flex items-start justify-between gap-2">
<div class="flex-1">
<p class="text-sm font-medium">{{ task.title }}</p>
{% if task.due_date %}
<div class="text-xs text-base-content/60 mt-1 flex items-center gap-1">
<span class="icon-[tabler--calendar] size-3"></span>
{{ task.due_date|date:"M d" }}
</div>
{% endif %}
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-xs btn-square">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
</svg>
</div>
<ul tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-32">
<li><a class="edit-task-btn" data-task-id="{{ task.id }}"
data-task-title="{{ task.title }}"
data-task-due-date="{{ task.due_date|date:'Y-m-d'|default:'' }}">Edit</a></li>
<li><a class="delete-task-btn" data-task-id="{{ task.id }}">Delete</a></li>
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- DONE Column -->
<div class="card bg-base-200 shadow-sm">
<div class="card-body p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="font-bold text-success uppercase text-sm tracking-wider">Done</h3>
<span class="badge badge-success badge-sm" id="done-count">{{ tasks_done|length }}</span>
</div>
<div id="kanban-done-list"
class="column-list flex flex-col gap-3 min-h-[150px] md:min-h-[200px] bg-base-100/50 p-2 rounded-lg"
data-status="DONE">
{% for task in tasks_done %}
<div class="task-item card bg-base-100 shadow-sm hover:shadow-md p-3 cursor-move" draggable="true"
data-task-id="{{ task.id }}" id="task-{{ task.id }}">
<div class="flex items-start justify-between gap-2">
<div class="flex-1">
<p class="text-sm font-medium">{{ task.title }}</p>
{% if task.due_date %}
<div class="text-xs text-base-content/60 mt-1 flex items-center gap-1">
<span class="icon-[tabler--calendar] size-3"></span>
{{ task.due_date|date:"M d" }}
</div>
{% endif %}
</div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-xs btn-square">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
</svg>
</div>
<ul tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-32">
<li><a class="edit-task-btn" data-task-id="{{ task.id }}"
data-task-title="{{ task.title }}"
data-task-due-date="{{ task.due_date|date:'Y-m-d'|default:'' }}">Edit</a></li>
<li><a class="delete-task-btn" data-task-id="{{ task.id }}">Delete</a></li>
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- Add Task Modal -->
<dialog id="add_task_modal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Add New Task</h3>
<form id="add-task-form" hx-post="{% url 'task_create' project_id=project.pk %}" hx-target="#kanban-todo-list"
hx-swap="afterbegin" class="space-y-4">
{% csrf_token %}
<div class="form-control">
<label class="label">
<span class="label-text">Task Title</span>
</label>
<input type="text" name="title" placeholder="Enter task title..." class="input input-bordered w-full"
required />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Due Date (Optional)</span>
</label>
<input type="date" name="due_date" class="input input-bordered w-full" />
</div>
<input type="hidden" name="status" value="TODO" />
<div class="modal-action">
<button type="button" class="btn"
onclick="add_task_modal.close(); document.getElementById('add-task-form').reset();">Cancel</button>
<button type="submit" class="btn btn-primary">Add Task</button>
</div>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<!-- Edit Task Modal -->
<dialog id="edit_task_modal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Edit Task</h3>
<form id="edit-task-form" hx-post="" hx-swap="outerHTML" class="space-y-4">
{% csrf_token %}
<input type="hidden" id="edit-task-id" name="task_id" />
<div class="form-control">
<label class="label">
<span class="label-text">Task Title</span>
</label>
<input type="text" id="edit-task-title" name="title" placeholder="Enter task title..."
class="input input-bordered w-full" required />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Due Date (Optional)</span>
</label>
<input type="date" id="edit-task-due-date" name="due_date" class="input input-bordered w-full" />
</div>
<div class="modal-action">
<button type="button" class="btn" onclick="edit_task_modal.close();">Cancel</button>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<script>
// Listen for successful task creation/edit to close modal and reset form
document.body.addEventListener('htmx:afterSwap', function (event) {
// Close add modal if task was added to todo list
if (event.detail.target.id === 'kanban-todo-list') {
add_task_modal.close();
document.getElementById('add-task-form').reset();
updateCounts();
}
// Close edit modal if a task was updated
if (event.detail.target.id && event.detail.target.id.startsWith('task-')) {
edit_task_modal.close();
// Re-setup drag and drop for the updated task
setupTaskDragDrop(event.detail.target);
}
});
// Function to setup drag and drop for a task element
function setupTaskDragDrop(task) {
task.addEventListener('dragstart', (e) => {
task.classList.add('opacity-50');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', task.dataset.taskId);
});
task.addEventListener('dragend', (e) => {
task.classList.remove('opacity-50');
});
}
// Setup drag and drop for all existing task items
document.querySelectorAll('.task-item').forEach(task => {
setupTaskDragDrop(task);
});
// Setup drop zones
document.querySelectorAll('.column-list').forEach(column => {
column.addEventListener('dragover', (e) => {
e.preventDefault();
const dragging = document.querySelector('.opacity-50');
const afterElement = getDragAfterElement(column, e.clientY);
if (afterElement == null) {
column.appendChild(dragging);
} else {
column.insertBefore(dragging, afterElement);
}
});
column.addEventListener('drop', (e) => {
e.preventDefault();
const taskId = e.dataTransfer.getData('text/plain');
const newStatus = column.dataset.status;
// Update task status via fetch
updateTaskStatus(taskId, newStatus);
updateCounts();
});
});
// Listen for HTMX afterSettle to setup drag-drop on new tasks
document.body.addEventListener('htmx:afterSettle', function (event) {
// Setup drag-drop for newly added tasks
// Check if the target contains task items
const newTasks = event.detail.target.querySelectorAll('.task-item');
newTasks.forEach(task => {
// Only setup if not already setup (check if it has the event listener)
if (!task.hasAttribute('data-drag-setup')) {
setupTaskDragDrop(task);
task.setAttribute('data-drag-setup', 'true');
}
});
});
function getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll('.task-item:not(.opacity-50)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
function updateTaskStatus(taskId, newStatus) {
// Use fetch to update task status
fetch(`/projects/tasks/${taskId}/update/`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: `status=${newStatus}`
});
}
// Event delegation for edit and delete buttons
document.addEventListener('click', function (e) {
// Handle edit button
if (e.target.classList.contains('edit-task-btn')) {
const taskId = e.target.dataset.taskId;
const taskTitle = e.target.dataset.taskTitle;
const taskDueDate = e.target.dataset.taskDueDate;
// Populate the edit form
document.getElementById('edit-task-id').value = taskId;
document.getElementById('edit-task-title').value = taskTitle;
document.getElementById('edit-task-due-date').value = taskDueDate;
// Set the form action URL
document.getElementById('edit-task-form').setAttribute(
'hx-post',
`/projects/tasks/${taskId}/edit/`
);
document.getElementById('edit-task-form').setAttribute(
'hx-target',
`#task-${taskId}`
);
// Reinitialize HTMX for the form
htmx.process(document.getElementById('edit-task-form'));
// Open the modal
edit_task_modal.showModal();
}
// Handle delete button
if (e.target.classList.contains('delete-task-btn')) {
const taskId = e.target.dataset.taskId;
if (confirm('Are you sure you want to delete this task?')) {
const taskElement = document.getElementById(`task-${taskId}`);
if (taskElement) {
// Use fetch to delete task
fetch(`/projects/tasks/${taskId}/delete/`, {
method: 'DELETE',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
}
}).then(() => {
taskElement.remove();
updateCounts();
});
}
}
}
});
function updateCounts() {
document.getElementById('todo-count').textContent =
document.getElementById('kanban-todo-list').querySelectorAll('.task-item').length;
document.getElementById('progress-count').textContent =
document.getElementById('kanban-progress-list').querySelectorAll('.task-item').length;
document.getElementById('review-count').textContent =
document.getElementById('kanban-review-list').querySelectorAll('.task-item').length;
document.getElementById('done-count').textContent =
document.getElementById('kanban-done-list').querySelectorAll('.task-item').length;
}
</script>