Provides admin UI to manage statuses and prevents moving tickets to completed statuses
252 lines
18 KiB
HTML
252 lines
18 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}Projects{% endblock %}
|
||
{% block breadcrumb %}
|
||
<nav class="mb-4" aria-label="Breadcrumb">
|
||
<ol class="flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||
<li><a href="{% url 'dashboard' %}" class="hover:text-gray-700 dark:hover:text-gray-300">Dashboard</a></li>
|
||
<li class="text-gray-300 dark:text-gray-600">/</li>
|
||
<li class="text-gray-900 dark:text-white font-medium" aria-current="page">Projects</li>
|
||
</ol>
|
||
</nav>
|
||
{% endblock %}
|
||
{% block content %}
|
||
<div x-data="{ columns: {{ columns|safe }}, openCols: false, selectedIds: [], selectAll: false }">
|
||
<div class="mb-6 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||
<div>
|
||
<h1 class="text-2xl font-bold">Projects</h1>
|
||
<p class="text-gray-500 dark:text-gray-400 text-sm mt-1">{{ page_obj.paginator.count }} total projects</p>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
{% if request.user.is_admin %}
|
||
<a href="{% url 'project-create' %}" class="dk-btn dk-btn-primary text-sm"><i class="fas fa-plus"></i> New Project</a>
|
||
{% endif %}
|
||
<form class="flex gap-2" method="get">
|
||
{% for c in columns %}<input type="hidden" name="columns" value="{{ c }}">{% endfor %}
|
||
{% if per_page != 10 %}<input type="hidden" name="per_page" value="{{ per_page }}">{% endif %}
|
||
<input type="text" name="search" value="{{ search }}" placeholder="Search by name or prefix..."
|
||
class="text-sm border border-gray-200 dark:border-gray-600 rounded-lg px-3 py-2 bg-white dark:bg-gray-950 focus:outline-none focus:ring-2 focus:ring-blue-500 w-56">
|
||
<button type="submit" class="dk-btn dk-btn-primary text-sm"><i class="fas fa-search"></i> Search</button>
|
||
{% if search %}
|
||
<a href="{% url 'projects-list' %}" class="dk-btn dk-btn-ghost text-sm"><i class="fas fa-times"></i> Clear</a>
|
||
{% endif %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<form id="projects-bulk-delete-form" method="post" action="{% url 'projects-bulk-delete' %}">
|
||
{% csrf_token %}
|
||
<template x-for="id in selectedIds" x-key="id">
|
||
<input type="hidden" name="ids" :value="id">
|
||
</template>
|
||
</form>
|
||
<div x-show="selectedIds.length > 0" x-cloak class="mb-4 flex items-center gap-3 px-4 py-2.5 bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-lg">
|
||
<span class="text-sm font-medium text-primary-700 dark:text-primary-300">
|
||
<span x-text="selectedIds.length"></span> selected
|
||
</span>
|
||
<button type="button" @click="confirmAction({title:'Delete Projects', message:'Delete ' + selectedIds.length + ' project(s)?', confirmText:'Delete', danger:true, onConfirm:function(){ document.getElementById('projects-bulk-delete-form').submit() }})" class="dk-btn dk-btn-danger text-xs px-3 py-1">
|
||
<i class="fas fa-trash mr-1"></i> Delete Selected
|
||
</button>
|
||
<button type="button" @click="selectedIds = []; selectAll = false" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 ml-auto">
|
||
<i class="fas fa-times mr-1"></i> Clear selection
|
||
</button>
|
||
</div>
|
||
|
||
<div class="rounded-xl bg-white dark:bg-gray-950 shadow-sm ring-1 ring-gray-950/5 dark:ring-white/10">
|
||
<div class="px-6 py-3 border-b border-gray-100 dark:border-gray-600 flex flex-wrap items-center justify-between gap-3">
|
||
<div class="flex items-center gap-3">
|
||
<a href="{% url 'projects-list' %}" class="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"><i class="fas fa-sync-alt mr-1"></i> Reset</a>
|
||
</div>
|
||
<div class="flex items-center gap-3">
|
||
<div class="relative">
|
||
<button @click="openCols = !openCols" class="text-xs flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300">
|
||
<i class="fas fa-columns text-[10px]"></i> Columns
|
||
</button>
|
||
<div x-show="openCols" @click.outside="openCols = false" x-cloak class="absolute right-0 mt-1 w-44 bg-white dark:bg-gray-950 rounded-lg shadow-lg ring-1 ring-gray-950/5 dark:ring-white/10 z-50 py-1">
|
||
<form method="get" id="columns-form">
|
||
<input type="hidden" name="search" value="{{ search }}">
|
||
{% if per_page != 10 %}<input type="hidden" name="per_page" value="{{ per_page }}">{% endif %}
|
||
{% for c in all_columns %}
|
||
<label class="flex items-center gap-2 px-3 py-1.5 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer">
|
||
<input type="checkbox" name="columns" value="{{ c }}" {% if c in columns %}checked{% endif %} onchange="document.getElementById('columns-form').submit()" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
||
{{ c|title }}
|
||
</label>
|
||
{% endfor %}
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<select name="per_page" onchange="this.form.submit()" form="columns-form" class="text-xs border border-gray-200 dark:border-gray-600 rounded-lg px-2 py-1.5 bg-white dark:bg-gray-950 text-gray-700 dark:text-gray-300">
|
||
<option value="5" {% if per_page == 5 %}selected{% endif %}>5 per page</option>
|
||
<option value="10" {% if per_page == 10 %}selected{% endif %}>10 per page</option>
|
||
<option value="25" {% if per_page == 25 %}selected{% endif %}>25 per page</option>
|
||
<option value="50" {% if per_page == 50 %}selected{% endif %}>50 per page</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full text-sm">
|
||
<thead>
|
||
<tr class="border-b border-gray-100 dark:border-gray-600 bg-gray-50 dark:bg-gray-800">
|
||
<th class="px-3 py-3 w-10">
|
||
<input type="checkbox" @change="selectAll = !selectAll; selectedIds = selectAll ? Array.from(document.querySelectorAll('.project-checkbox')).map(cb => cb.value) : []" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
||
</th>
|
||
{% if "color" in columns %}<th class="px-2 py-3 w-10"></th>{% endif %}
|
||
{% if "name" in columns %}<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||
<a href="?sort=name&order={% if sort == 'name' and order == 'asc' %}desc{% else %}asc{% endif %}{% if search %}&search={{ search }}{% endif %}{% for c in columns %}&columns={{ c }}{% endfor %}&per_page={{ per_page }}" class="hover:text-gray-700 dark:hover:text-gray-300">
|
||
Name {% if sort == 'name' %}<i class="fas fa-sort-{{ order }}-alt text-[10px]"></i>{% endif %}
|
||
</a>
|
||
</th>{% endif %}
|
||
{% if "ticket_prefix" in columns %}<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Prefix</th>{% endif %}
|
||
{% if "progress" in columns %}<th class="px-4 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Progress</th>{% endif %}
|
||
{% if "start_date" in columns %}<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||
<a href="?sort=start_date&order={% if sort == 'start_date' and order == 'asc' %}desc{% else %}asc{% endif %}{% if search %}&search={{ search }}{% endif %}{% for c in columns %}&columns={{ c }}{% endfor %}&per_page={{ per_page }}" class="hover:text-gray-700 dark:hover:text-gray-300">
|
||
Start Date {% if sort == 'start_date' %}<i class="fas fa-sort-{{ order }}-alt text-[10px]"></i>{% endif %}
|
||
</a>
|
||
</th>{% endif %}
|
||
{% if "end_date" in columns %}<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">End Date</th>{% endif %}
|
||
{% if "remaining_days" in columns %}<th class="px-4 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Remaining Days</th>{% endif %}
|
||
{% if "pinned" in columns %}<th class="px-4 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Pinned</th>{% endif %}
|
||
{% if "members" in columns %}<th class="px-4 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||
<a href="?sort=members&order={% if sort == 'members' and order == 'asc' %}desc{% else %}asc{% endif %}{% if search %}&search={{ search }}{% endif %}{% for c in columns %}&columns={{ c }}{% endfor %}&per_page={{ per_page }}" class="hover:text-gray-700 dark:hover:text-gray-300">
|
||
Members {% if sort == 'members' %}<i class="fas fa-sort-{{ order }}-alt text-[10px]"></i>{% endif %}
|
||
</a>
|
||
</th>{% endif %}
|
||
{% if "tickets" in columns %}<th class="px-4 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||
<a href="?sort=tickets&order={% if sort == 'tickets' and order == 'asc' %}desc{% else %}asc{% endif %}{% if search %}&search={{ search }}{% endif %}{% for c in columns %}&columns={{ c }}{% endfor %}&per_page={{ per_page }}" class="hover:text-gray-700 dark:hover:text-gray-300">
|
||
Tickets {% if sort == 'tickets' %}<i class="fas fa-sort-{{ order }}-alt text-[10px]"></i>{% endif %}
|
||
</a>
|
||
</th>{% endif %}
|
||
{% if "created_at" in columns %}<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Created</th>{% endif %}
|
||
{% if "updated_at" in columns %}<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Updated</th>{% endif %}
|
||
<th class="px-4 py-3 text-right text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
||
{% for p in projects %}
|
||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors" :class="selectedIds.includes('{{ p.id }}') && 'bg-blue-50 dark:bg-blue-900/10'">
|
||
<td class="px-3 py-3">
|
||
<input type="checkbox" class="project-checkbox rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" value="{{ p.id }}" x-model="selectedIds">
|
||
</td>
|
||
{% if "color" in columns %}
|
||
<td class="px-2 py-3">
|
||
<div class="w-6 h-6 rounded" style="background-color:{{ p.color|default:'#6B7280' }}"></div>
|
||
</td>
|
||
{% endif %}
|
||
{% if "name" in columns %}
|
||
<td class="px-4 py-3">
|
||
<a href="{% url 'project-detail' p.id %}" class="font-medium text-gray-900 dark:text-white hover:text-primary">{{ p.name }}</a>
|
||
</td>
|
||
{% endif %}
|
||
{% if "ticket_prefix" in columns %}
|
||
<td class="px-4 py-3">
|
||
<span class="inline-flex px-2 py-0.5 rounded text-xs font-semibold text-white" style="background-color:{{ p.color|default:'#3B82F6' }}">{{ p.ticket_prefix }}</span>
|
||
</td>
|
||
{% endif %}
|
||
{% if "progress" in columns %}
|
||
<td class="px-4 py-3 text-center">
|
||
{% with prog=p.progress_percentage %}
|
||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
|
||
{% if prog >= 100 %}bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400
|
||
{% elif prog >= 75 %}bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400
|
||
{% elif prog >= 50 %}bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400
|
||
{% elif prog >= 25 %}bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300
|
||
{% else %}bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400{% endif %}">
|
||
{{ prog }}%
|
||
</span>
|
||
{% endwith %}
|
||
</td>
|
||
{% endif %}
|
||
{% if "start_date" in columns %}
|
||
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">{{ p.start_date|date:"d/m/Y"|default:"—" }}</td>
|
||
{% endif %}
|
||
{% if "end_date" in columns %}
|
||
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">{{ p.end_date|date:"d/m/Y"|default:"—" }}</td>
|
||
{% endif %}
|
||
{% if "remaining_days" in columns %}
|
||
<td class="px-4 py-3 text-center">
|
||
{% if p.remaining_days is None %}
|
||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400">—</span>
|
||
{% else %}
|
||
{% with days=p.remaining_days %}
|
||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
|
||
{% if days <= 0 %}bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400
|
||
{% elif days <= 7 %}bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400
|
||
{% else %}bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400{% endif %}">
|
||
{{ days }} days
|
||
</span>
|
||
{% endwith %}
|
||
{% endif %}
|
||
</td>
|
||
{% endif %}
|
||
{% if "pinned" in columns %}
|
||
<td class="px-4 py-3 text-center">
|
||
<form method="post" action="{% url 'project-toggle-pin' p.id %}">
|
||
{% csrf_token %}
|
||
<button type="submit" onclick="event.preventDefault(); this.closest('form').submit()" role="switch"
|
||
class="relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
|
||
style="background-color:{% if p.pinned_date %}var(--color-primary, #3b82f6){% else %}#d1d5db{% endif %};">
|
||
<span class="pointer-events-none relative inline-block h-4 w-4 rounded-full bg-white shadow-sm ring-0 transition duration-200 ease-in-out"
|
||
style="transform:{% if p.pinned_date %}translateX(16px){% else %}translateX(0){% endif %};"></span>
|
||
</button>
|
||
</form>
|
||
</td>
|
||
{% endif %}
|
||
{% if "members" in columns %}
|
||
<td class="px-4 py-3 text-center text-sm font-medium text-gray-900 dark:text-white">{{ p.member_count }}</td>
|
||
{% endif %}
|
||
{% if "tickets" in columns %}
|
||
<td class="px-4 py-3 text-center text-sm font-medium text-gray-900 dark:text-white">{{ p.ticket_count }}</td>
|
||
{% endif %}
|
||
{% if "created_at" in columns %}
|
||
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">{{ p.created_at|date:"d/m/Y H:i" }}</td>
|
||
{% endif %}
|
||
{% if "updated_at" in columns %}
|
||
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">{{ p.updated_at|date:"d/m/Y H:i" }}</td>
|
||
{% endif %}
|
||
<td class="px-4 py-3 text-right">
|
||
<div class="flex items-center justify-end gap-1">
|
||
<a href="{% url 'project-detail' p.id %}" class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors shadow-sm">View</a>
|
||
<a href="{% url 'project-edit' p.id %}" class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors shadow-sm">Edit</a>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr>
|
||
<td colspan="20" class="text-center py-12 text-gray-400">
|
||
<i class="fas fa-folder-open text-4xl mb-3 block text-gray-300 dark:text-gray-600"></i>
|
||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">No projects yet</p>
|
||
{% if request.user.is_admin %}
|
||
<a href="{% url 'project-create' %}" class="text-primary text-sm mt-2 inline-block hover:underline">Create your first project</a>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{% if page_obj.has_other_pages %}
|
||
<div class="px-6 py-3 border-t border-gray-100 dark:border-gray-600 flex items-center justify-between">
|
||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||
Showing {{ page_obj.start_index }}–{{ page_obj.end_index }} of {{ page_obj.paginator.count }}
|
||
</p>
|
||
<div class="flex items-center gap-1">
|
||
{% if page_obj.has_previous %}
|
||
<a href="?page={{ page_obj.previous_page_number }}{% if search %}&search={{ search }}{% endif %}{% for c in columns %}&columns={{ c }}{% endfor %}{% if per_page != 10 %}&per_page={{ per_page }}{% endif %}&sort={{ sort }}&order={{ order }}" class="px-2.5 py-1 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800">Previous</a>
|
||
{% endif %}
|
||
{% for n in page_obj.paginator.page_range %}
|
||
{% if n == page_obj.number %}
|
||
<span class="px-2.5 py-1 rounded text-xs font-medium bg-blue-600 text-white">{{ n }}</span>
|
||
{% elif n >= page_obj.number|add:"-2" and n <= page_obj.number|add:"2" %}
|
||
<a href="?page={{ n }}{% if search %}&search={{ search }}{% endif %}{% for c in columns %}&columns={{ c }}{% endfor %}{% if per_page != 10 %}&per_page={{ per_page }}{% endif %}&sort={{ sort }}&order={{ order }}" class="px-2.5 py-1 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800">{{ n }}</a>
|
||
{% endif %}
|
||
{% endfor %}
|
||
{% if page_obj.has_next %}
|
||
<a href="?page={{ page_obj.next_page_number }}{% if search %}&search={{ search }}{% endif %}{% for c in columns %}&columns={{ c }}{% endfor %}{% if per_page != 10 %}&per_page={{ per_page }}{% endif %}&sort={{ sort }}&order={{ order }}" class="px-2.5 py-1 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800">Next</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endblock %} |