feat(projects): add project management templates

Added new templates for project management functionality including listing, creation, and detailed views. These templates integrate with HTMX for dynamic interactions and provide a responsive layout.

**New Features**
- **project_list.html**: Displays a table of projects with status badges, client associations, and deadline tracking. Includes actions for creating, editing, and deleting projects.
- **project_detail.html**: Shows detailed project information including status, description, client, and deadline. Features an embedded kanban board for task management and a modal for task creation.
- **project_form.html**: Provides a multi-step form wizard for creating/editing projects with validation and navigation controls.

**Enhancements & Refinements**
- Responsive design across all templates for consistent UX on different devices.
- Dynamic status badges color-coded based on project state.
- Integrated HTMX for seamless form submissions and DOM updates in project list and task management.
This commit is contained in:
sankalp committed 2025-12-14 09:39:12 +05:45
1 parent 036a6471a3
commit 31688a6b04
3 files changed
+244

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
{% extends "shared/base.html" %}
{% load static %}
{% block content %}
{% include "shared/navbar.html" %}
<div class="pt-24 px-4 max-w-7xl mx-auto min-h-screen flex flex-col pb-10">
<!-- Header -->
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
<div>
<h1 class="text-3xl font-bold flex items-center gap-3">
{{ project.name }}
<span
class="badge badge-lg {% if project.status == 'COMPLETED' %}badge-success{% elif project.status == 'IN_PROGRESS' %}badge-info{% elif project.status == 'ON_HOLD' %}badge-warning{% elif project.status == 'CANCELLED' %}badge-error{% else %}badge-ghost{% endif %}">
{{ project.get_status_display }}
</span>
</h1>
<p class="text-base-content/70 mt-1">
{{ project.client.name }} • Due: {{ project.deadline|default:"No deadline" }}
</p>
</div>
<div class="flex gap-2">
<a href="{% url 'project_edit' pk=project.pk %}" class="btn btn-outline">Edit Project</a>
</div>
</div>
<!-- Description -->
<div class="bg-base-100 p-6 rounded-box shadow-sm mb-8">
<h3 class="font-bold text-lg mb-2">Description</h3>
<p class="whitespace-pre-wrap">{{ project.description }}</p>
</div>
<!-- Kanban Board -->
<div class="bg-base-100 p-6 rounded-box shadow-sm">
<div class="flex items-center justify-between mb-6">
<h3 class="font-bold text-lg">Tasks</h3>
</div>
<div id="kanban-board">
{% include "projects/partials/kanban_board.html" %}
</div>
</div>
</div>
<!-- Task Modal -->
<dialog id="task_modal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Add New Task</h3>
<form hx-post="{% url 'task_create' project_id=project.pk %}" hx-target="#kanban-todo-list" hx-swap="beforeend"
hx-on::after-request="if(event.detail.successful) { this.reset(); task_modal.close(); }" class="space-y-4">
{% csrf_token %}
{% for field in task_form %}
<div class="form-control">
<label class="label" for="{{ field.id_for_label }}">
<span class="label-text">{{ field.label }}</span>
</label>
{{ field }}
</div>
{% endfor %}
<div class="modal-action">
<button type="button" class="btn" onclick="task_modal.close()">Cancel</button>
<button type="submit" class="btn btn-primary">Create Task</button>
</div>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
{% endblock %}
+106
View File
@@ -0,0 +1,106 @@
{% extends "shared/base.html" %}
{% load static %}
{% block content %}
{% include "shared/navbar.html" %}
<div class="pt-24 px-4 max-w-7xl mx-auto min-h-screen flex flex-col pb-10">
<h2 class="text-2xl font-bold mb-6">{% if project %}Edit Project{% else %}New Project{% endif %}</h2>
{% include "projects/partials/project_form.html" %}
</div>
<script>
(function () {
const stepper = document.getElementById('wizard-validation-horizontal');
if (!stepper) return;
const navItems = stepper.querySelectorAll('[data-stepper-nav-item]');
const contentItems = stepper.querySelectorAll('[data-stepper-content-item]');
const backBtn = stepper.querySelector('[data-stepper-back-btn]');
const nextBtn = stepper.querySelector('[data-stepper-next-btn]');
const finishBtn = stepper.querySelector('[data-stepper-finish-btn]');
let currentStep = 1;
const totalSteps = 3;
function updateStepper() {
// Update Nav
navItems.forEach(item => {
const index = parseInt(JSON.parse(item.dataset.stepperNavItem).index);
const circle = item.querySelector('span > span');
const checkIcon = circle.querySelector('.icon-\\[tabler--check\\]');
const numberSpan = circle.querySelector('span:not([class*="icon"])');
// Reset classes
circle.classList.remove('bg-brand', 'text-white', 'bg-success', 'text-white');
circle.classList.add('text-bg-soft-neutral');
checkIcon.classList.add('hidden');
numberSpan.classList.remove('hidden');
if (index < currentStep) {
// Completed
circle.classList.remove('text-bg-soft-neutral');
circle.classList.add('bg-success', 'text-white');
checkIcon.classList.remove('hidden');
numberSpan.classList.add('hidden');
} else if (index === currentStep) {
// Active
circle.classList.remove('text-bg-soft-neutral');
circle.classList.add('bg-brand', 'text-white');
}
});
// Update Content
contentItems.forEach(item => {
const index = parseInt(JSON.parse(item.dataset.stepperContentItem).index);
if (index === currentStep) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
// Update Buttons
if (currentStep === 1) {
backBtn.style.display = 'none';
nextBtn.style.display = 'inline-flex';
finishBtn.style.display = 'none';
} else if (currentStep === totalSteps) {
backBtn.style.display = 'inline-flex';
nextBtn.style.display = 'none';
finishBtn.style.display = 'inline-flex';
} else {
backBtn.style.display = 'inline-flex';
nextBtn.style.display = 'inline-flex';
finishBtn.style.display = 'none';
}
}
nextBtn.addEventListener('click', () => {
// Simple validation check for current step inputs
const currentContent = stepper.querySelector(`[data-stepper-content-item='{ "index": ${currentStep} }']`);
const inputs = currentContent.querySelectorAll('input, textarea, select');
let valid = true;
inputs.forEach(input => {
if (!input.checkValidity()) {
valid = false;
input.reportValidity();
}
});
if (valid && currentStep < totalSteps) {
currentStep++;
updateStepper();
}
});
backBtn.addEventListener('click', () => {
if (currentStep > 1) {
currentStep--;
updateStepper();
}
});
// Initialize
updateStepper();
})();
</script>
{% endblock %}
+69
View File
@@ -0,0 +1,69 @@
{% extends "shared/base.html" %}
{% load static %}
{% block content %}
{% include "shared/navbar.html" %}
<div class="pt-24 px-4 max-w-7xl mx-auto min-h-screen flex flex-col pb-10">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">Projects</h1>
<a href="{% url 'project_create' %}" class="btn btn-primary">
<span class="icon-[tabler--plus] size-5"></span>
New Project
</a>
</div>
<div class="overflow-x-auto bg-base-100 rounded-box shadow">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Client</th>
<th>Status</th>
<th>Deadline</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for project in projects %}
<tr class="hover">
<td>
<a href="{% url 'project_detail' pk=project.pk %}" class="font-bold hover:text-primary">
{{ project.name }}
</a>
<div class="text-xs opacity-50">{{ project.description|truncatechars:50 }}</div>
</td>
<td>{{ project.client.name }}</td>
<td>
<div
class="badge {% if project.status == 'COMPLETED' %}badge-success{% elif project.status == 'IN_PROGRESS' %}badge-info{% elif project.status == 'ON_HOLD' %}badge-warning{% elif project.status == 'CANCELLED' %}badge-error{% else %}badge-ghost{% endif %}">
{{ project.get_status_display }}
</div>
</td>
<td>{{ project.deadline|default:"-" }}</td>
<td>
<div class="flex gap-2">
<a href="{% url 'project_edit' pk=project.pk %}" class="btn btn-sm btn-outline">
<span class="icon-[tabler--edit] size-4"></span>
Edit
</a>
<button class="btn btn-sm btn-outline btn-error"
hx-delete="{% url 'project_delete' pk=project.pk %}" hx-confirm="Are you sure?"
hx-target="closest tr" hx-swap="outerHTML">
<span class="icon-[tabler--trash] size-4"></span>
Delete
</button>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center py-10 text-base-content/50">
No projects found. Create one to get started.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}