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:
3 files changed
+244
No files matched your search
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
Reference in new issue
Block a user