feat(templates/projects): add manage statuses button and restricted mode UI

Provides admin UI to manage statuses and prevents moving tickets to completed statuses
This commit is contained in:
sankalp committed 2026-06-28 19:15:35 +05:45
1 parent 04a7965a2a
commit 457fc76f9d
5 files changed
+314 -23

No files matched your search

+153 -4
View File
@@ -33,6 +33,18 @@
newTicketOpen: false, newTicketOpen: false,
exportOpen: false, exportOpen: false,
filterOpen: false, filterOpen: false,
manageStatusesOpen: false,
createStatusName: '',
createStatusColor: '#6B7280',
createStatusOrder: '0',
createStatusCompleted: false,
editStatusId: null,
editStatusName: '',
editStatusColor: '',
editStatusOrder: '0',
editStatusCompleted: false,
ticketName: '', ticketName: '',
ticketDescription: '', ticketDescription: '',
@@ -187,6 +199,9 @@
touch.clientY >= rect.top && touch.clientY <= rect.bottom) targetColumn = col; touch.clientY >= rect.top && touch.clientY <= rect.bottom) targetColumn = col;
col.classList.remove('bg-blue-50', 'dark:bg-blue-900/20'); col.classList.remove('bg-blue-50', 'dark:bg-blue-900/20');
}); });
{% if not can_move_to_completed %}
if (targetColumn && targetColumn.getAttribute('data-status-completed') === 'true') targetColumn = null;
{% endif %}
if (targetColumn && targetColumn !== originalColumn) if (targetColumn && targetColumn !== originalColumn)
this.moveTicketToStatus(this.draggingTicket, targetColumn.getAttribute('data-status-id')); this.moveTicketToStatus(this.draggingTicket, targetColumn.getAttribute('data-status-id'));
this.draggingTicket = null; this.draggingTicket = null;
@@ -209,6 +224,9 @@
col.addEventListener('drop', (e) => { col.addEventListener('drop', (e) => {
e.preventDefault(); e.preventDefault();
col.classList.remove('bg-blue-50', 'dark:bg-blue-900/20'); col.classList.remove('bg-blue-50', 'dark:bg-blue-900/20');
{% if not can_move_to_completed %}
if (col.getAttribute('data-status-completed') === 'true') return;
{% endif %}
if (this.draggingTicket) { if (this.draggingTicket) {
this.moveTicketToStatus(this.draggingTicket, col.getAttribute('data-status-id')); this.moveTicketToStatus(this.draggingTicket, col.getAttribute('data-status-id'));
this.draggingTicket = null; this.draggingTicket = null;
@@ -297,6 +315,11 @@ class="space-y-6"
<button @click="filterOpen = true" class="dk-btn dk-btn-ghost text-sm text-blue-600"> <button @click="filterOpen = true" class="dk-btn dk-btn-ghost text-sm text-blue-600">
<i class="fas fa-filter"></i> Filter {% if selected_user_ids %}<span class="ml-1 bg-blue-100 text-blue-800 text-xs font-semibold px-2 py-0.5 rounded">{{ selected_user_ids|length }}</span>{% endif %} <i class="fas fa-filter"></i> Filter {% if selected_user_ids %}<span class="ml-1 bg-blue-100 text-blue-800 text-xs font-semibold px-2 py-0.5 rounded">{{ selected_user_ids|length }}</span>{% endif %}
</button> </button>
{% if is_admin %}
<button @click="manageStatusesOpen = true" class="dk-btn dk-btn-ghost text-sm text-purple-600">
<i class="fas fa-cog"></i> Manage Statuses
</button>
{% endif %}
<button @click="newTicketOpen = true" class="dk-btn dk-btn-primary text-sm"> <button @click="newTicketOpen = true" class="dk-btn dk-btn-primary text-sm">
<i class="fas fa-plus"></i> New Ticket <i class="fas fa-plus"></i> New Ticket
</button> </button>
@@ -345,6 +368,12 @@ class="space-y-6"
<i class="fas fa-eye"></i><span class="text-sm font-medium">View Only Mode</span><span class="text-xs opacity-75">You can view tickets but cannot move them</span> <i class="fas fa-eye"></i><span class="text-sm font-medium">View Only Mode</span><span class="text-xs opacity-75">You can view tickets but cannot move them</span>
</div> </div>
</div> </div>
{% elif not can_move_to_completed %}
<div class="flex justify-center mb-4">
<div class="inline-flex items-center gap-2 px-4 py-2 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg text-blue-800 dark:text-blue-200">
<i class="fas fa-info-circle"></i><span class="text-sm font-medium">Restricted Mode</span><span class="text-xs opacity-75">You can move tickets to active statuses only</span>
</div>
</div>
{% endif %} {% endif %}
@@ -354,7 +383,7 @@ class="space-y-6"
<div class="inline-flex gap-4 pb-2 min-w-full"> <div class="inline-flex gap-4 pb-2 min-w-full">
{% for col in columns %} {% for col in columns %}
<div class="status-column rounded-xl border border-gray-200 dark:border-gray-600 flex flex-col bg-gray-50 dark:bg-gray-950 w-[calc(85vw-2rem)] min-w-[280px] max-w-[350px] h-[700px] sm:w-[calc((100vw-6rem)/2)] sm:h-[750px] lg:w-[calc((100vw-8rem)/3)] lg:h-[800px] xl:w-[calc((100vw-10rem)/4)] xl:h-[850px]" <div class="status-column rounded-xl border border-gray-200 dark:border-gray-600 flex flex-col bg-gray-50 dark:bg-gray-950 w-[calc(85vw-2rem)] min-w-[280px] max-w-[350px] h-[700px] sm:w-[calc((100vw-6rem)/2)] sm:h-[750px] lg:w-[calc((100vw-8rem)/3)] lg:h-[800px] xl:w-[calc((100vw-10rem)/4)] xl:h-[850px]"
data-status-id="{{ col.status.id }}"> data-status-id="{{ col.status.id }}" data-status-completed="{{ col.status.is_completed|lower }}">
<div class="px-4 py-3 rounded-t-xl border-b border-gray-200 dark:border-gray-600 flex-shrink-0" <div class="px-4 py-3 rounded-t-xl border-b border-gray-200 dark:border-gray-600 flex-shrink-0"
style="background-color:{{ col.status.color|default:'#f3f4f6' }};"> style="background-color:{{ col.status.color|default:'#f3f4f6' }};">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -416,11 +445,11 @@ class="space-y-6"
{% if ticket.assignees.all %} {% if ticket.assignees.all %}
{% for assignee in ticket.assignees.all|slice:":2" %} {% for assignee in ticket.assignees.all|slice:":2" %}
<div class="inline-flex items-center px-2 py-1 rounded-full bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 gap-1"> <div class="inline-flex items-center px-2 py-1 rounded-full bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 gap-1">
<span class="w-4 h-4 rounded-full bg-blue-500 flex items-center justify-center text-[10px] text-white flex-shrink-0">{{ assignee.get_full_name|default:assignee.username|first|upper }}</span> <span title="{{ assignee.get_full_name|default:assignee.username }}" class="w-4 h-4 rounded-full bg-blue-500 flex items-center justify-center text-[10px] text-white flex-shrink-0">{{ assignee.get_full_name|default:assignee.username|first|upper }}</span>
<span class="text-xs font-medium truncate max-w-[60px]">{{ assignee.get_full_name|default:assignee.username|truncatechars:8 }}</span> <span title="{{ assignee.get_full_name|default:assignee.username }}" class="text-xs font-medium truncate max-w-[60px]">{{ assignee.get_full_name|default:assignee.username|truncatechars:8 }}</span>
</div> </div>
{% endfor %} {% endfor %}
{% if ticket.assignees.all|length > 2 %}<div class="inline-flex items-center px-2 py-1 rounded-full bg-gray-100 dark:bg-gray-950 text-gray-700 dark:text-gray-400"><span class="text-xs font-medium">+{{ ticket.assignees.all|length|add:"-2" }}</span></div>{% endif %} {% if ticket.assignees.all|length > 2 %}<div title="View all assignees" class="inline-flex items-center px-2 py-1 rounded-full bg-gray-100 dark:bg-gray-950 text-gray-700 dark:text-gray-400"><span class="text-xs font-medium">+{{ ticket.assignees.all|length|add:"-2" }}</span></div>{% endif %}
{% else %} {% else %}
<div class="inline-flex items-center px-2 py-1 rounded-full bg-gray-100 dark:bg-gray-950 text-gray-700 dark:text-gray-400"> <div class="inline-flex items-center px-2 py-1 rounded-full bg-gray-100 dark:bg-gray-950 text-gray-700 dark:text-gray-400">
<i class="fas fa-user text-gray-400 dark:text-gray-500 mr-1 text-xs"></i><span class="text-xs font-medium">Unassigned</span> <i class="fas fa-user text-gray-400 dark:text-gray-500 mr-1 text-xs"></i><span class="text-xs font-medium">Unassigned</span>
@@ -609,6 +638,126 @@ class="space-y-6"
</div> </div>
</div> </div>
<!-- Manage Statuses Modal -->
<div x-cloak x-show="manageStatusesOpen" class="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
@keydown.escape.window="manageStatusesOpen = false">
<div class="fixed inset-0 bg-black/50" @click="manageStatusesOpen = false"></div>
<div class="relative bg-white dark:bg-gray-950 rounded-xl shadow-xl border border-gray-200 dark:border-gray-600 w-full max-w-6xl mx-auto my-auto max-h-[90vh] overflow-y-auto z-10 p-6 md:p-8">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Manage Ticket Statuses</h3>
<button @click="manageStatusesOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"><i class="fas fa-times text-xl"></i></button>
</div>
<!-- Create Status Form -->
<div class="rounded-lg bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 mb-4">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-3">New Status</h4>
<form method="post" action="{% url 'ticket-status-create' %}" class="grid grid-cols-12 gap-3 items-end">
{% csrf_token %}
<input type="hidden" name="project_id" value="{{ selected_project.id }}">
<div class="col-span-3">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Name</label>
<input type="text" name="name" x-model="createStatusName" required
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
</div>
<div class="col-span-2">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Color</label>
<div class="flex items-center gap-2">
<toolcool-color-picker popup-position="left" :color="createStatusColor" @change="createStatusColor = $event.detail.hex" class="shrink-0"></toolcool-color-picker>
<input type="text" name="color" x-model="createStatusColor" required
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none uppercase font-mono">
</div>
</div>
<div class="col-span-2">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Order</label>
<input type="number" name="sort_order" x-model="createStatusOrder" required
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
</div>
<div class="col-span-3 flex items-center gap-3 pb-1">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="is_completed" x-model="createStatusCompleted" class="w-4 h-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500">
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">Completed</span>
</label>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors">Add</button>
</div>
</form>
</div>
<!-- Status List -->
<div class="rounded-xl bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Color</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Order</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Completed</th>
<th class="px-4 py-3 text-right text-xs font-medium 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 col in columns %}
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30" x-data="{ editing: false }">
<td class="px-4 py-3 font-medium text-gray-900 dark:text-white">{{ col.status.name }}</td>
<td class="px-4 py-3">
<span class="inline-flex items-center gap-1.5 text-xs">
<span class="w-3 h-3 rounded-full inline-block" style="background-color:{{ col.status.color }}"></span>{{ col.status.color }}
</span>
</td>
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ col.status.sort_order }}</td>
<td class="px-4 py-3">
{% if col.status.is_completed %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400">Yes</span>
{% else %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400">No</span>
{% endif %}
</td>
<td class="px-4 py-3 text-right whitespace-nowrap">
<!-- Edit form (inline toggle) -->
<form x-show="editing" x-cloak method="post" action="/ticket-statuses/{{ col.status.id }}/edit/" class="flex items-center gap-2">
{% csrf_token %}
<input type="text" name="name" :value="editStatusName" required
class="w-28 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-2 py-1 text-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
<div class="flex items-center gap-1">
<toolcool-color-picker popup-position="left" :color="editStatusColor" @change="editStatusColor = $event.detail.hex" class="shrink-0" style="height:28px;"></toolcool-color-picker>
<input type="text" name="color" x-model="editStatusColor" required
class="w-20 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-2 py-1 text-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none uppercase font-mono">
</div>
<input type="number" name="sort_order" x-model="editStatusOrder" required
class="w-16 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-2 py-1 text-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
<label class="flex items-center gap-1 text-xs whitespace-nowrap">
<input type="checkbox" name="is_completed" x-model="editStatusCompleted" class="w-3 h-3 text-blue-600 rounded border-gray-300">
Done
</label>
<button type="submit" class="px-2 py-1 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700"><i class="fas fa-check"></i></button>
<button type="button" @click="editing = false" class="px-2 py-1 text-xs font-medium text-gray-600 bg-gray-100 dark:bg-gray-800 rounded hover:bg-gray-200"><i class="fas fa-times"></i></button>
</form>
<!-- Action buttons (hidden when editing) -->
<div x-show="!editing" class="flex items-center gap-1">
<button type="button" @click="editStatusId='{{ col.status.id }}'; editStatusName='{{ col.status.name|escapejs }}'; editStatusColor='{{ col.status.color|escapejs }}'; editStatusOrder='{{ col.status.sort_order }}'; editStatusCompleted={{ col.status.is_completed|lower }}; editing = true" class="inline-flex items-center gap-1 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">
<i class="fas fa-pen"></i>
</button>
<form method="post" action="{% url 'ticket-status-delete' col.status.id %}" class="inline">
{% csrf_token %}
<input type="hidden" name="project_id" value="{{ selected_project.id }}">
<button type="button" @click="confirmAction({title:'Delete Status', message:'Delete this status? This cannot be undone.', confirmText:'Delete', danger:true, onConfirm:function(){ $el.closest('form').submit() }})" class="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium text-red-600 dark:text-red-400 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors shadow-sm">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="px-4 py-8 text-center text-sm text-gray-400">No statuses found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}
+151 -1
View File
@@ -174,6 +174,11 @@
<div class="px-6 py-4 border-b border-gray-100 dark:border-gray-700 flex flex-wrap items-center justify-between gap-4"> <div class="px-6 py-4 border-b border-gray-100 dark:border-gray-700 flex flex-wrap items-center justify-between gap-4">
<h3 class="text-base font-semibold text-gray-900 dark:text-white">Ticket Statuses</h3> <h3 class="text-base font-semibold text-gray-900 dark:text-white">Ticket Statuses</h3>
<div class="flex items-center gap-3 flex-wrap"> <div class="flex items-center gap-3 flex-wrap">
{% if is_admin %}
<button type="button" @click="$store.projectDetail.manageStatusesOpen = true" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-purple-600 bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-colors">
<i class="fas fa-cog"></i> Manage
</button>
{% endif %}
<form method="get" class="flex items-center gap-2"> <form method="get" class="flex items-center gap-2">
<div class="relative"> <div class="relative">
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs"></i> <i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs"></i>
@@ -203,6 +208,7 @@
{% if "color" in statuses_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Color</th>{% endif %} {% if "color" in statuses_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Color</th>{% endif %}
{% if "sort_order" in statuses_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Order</th>{% endif %} {% if "sort_order" in statuses_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Order</th>{% endif %}
{% if "is_completed" in statuses_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Completed</th>{% endif %} {% if "is_completed" in statuses_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Completed</th>{% endif %}
{% if is_admin %}<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>{% endif %}
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-700"> <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
@@ -212,9 +218,23 @@
{% if "color" in statuses_columns %}<td class="px-4 py-3"><span class="inline-flex items-center gap-1.5 text-xs"><span class="w-3 h-3 rounded-full flex-shrink-0" style="background-color:{{ s.color }}"></span>{{ s.color }}</span></td>{% endif %} {% if "color" in statuses_columns %}<td class="px-4 py-3"><span class="inline-flex items-center gap-1.5 text-xs"><span class="w-3 h-3 rounded-full flex-shrink-0" style="background-color:{{ s.color }}"></span>{{ s.color }}</span></td>{% endif %}
{% if "sort_order" in statuses_columns %}<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ s.sort_order }}</td>{% endif %} {% if "sort_order" in statuses_columns %}<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ s.sort_order }}</td>{% endif %}
{% if "is_completed" in statuses_columns %}<td class="px-4 py-3">{% if s.is_completed %}<span class="px-2 py-0.5 rounded-full text-xs bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">Yes</span>{% else %}<span class="text-gray-400 text-xs">No</span>{% endif %}</td>{% endif %} {% if "is_completed" in statuses_columns %}<td class="px-4 py-3">{% if s.is_completed %}<span class="px-2 py-0.5 rounded-full text-xs bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">Yes</span>{% else %}<span class="text-gray-400 text-xs">No</span>{% endif %}</td>{% endif %}
{% if is_admin %}
<td class="px-4 py-3 text-right whitespace-nowrap">
<a href="{% url 'project-edit' project.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 mr-1">
<i class="fas fa-pen"></i>
</a>
<form method="post" action="{% url 'ticket-status-delete' s.id %}" class="inline">
{% csrf_token %}
<input type="hidden" name="project_id" value="{{ project.id }}">
<button type="button" @click="confirmAction({title:'Delete Status', message:'Are you sure you want to delete this status? This cannot be undone.', confirmText:'Delete', danger:true, onConfirm:function(){ $el.closest('form').submit() }})" class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-red-600 dark:text-red-400 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors shadow-sm">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
{% endif %}
</tr> </tr>
{% empty %} {% empty %}
<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-gray-400">No statuses found.</td></tr> <tr><td colspan="{% if is_admin %}5{% else %}4{% endif %}" class="px-4 py-8 text-center text-sm text-gray-400">No statuses found.</td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
@@ -558,6 +578,126 @@
{% include "components/external_dashboard_modal.html" with show_state="$store.projectDetail.externalModalOpen" %} {% include "components/external_dashboard_modal.html" with show_state="$store.projectDetail.externalModalOpen" %}
<!-- Manage Statuses Modal -->
<div x-cloak x-show="$store.projectDetail.manageStatusesOpen" class="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
@keydown.escape.window="$store.projectDetail.manageStatusesOpen = false">
<div class="fixed inset-0 bg-black/50" @click="$store.projectDetail.manageStatusesOpen = false"></div>
<div class="relative bg-white dark:bg-gray-950 rounded-xl shadow-xl border border-gray-200 dark:border-gray-600 w-full max-w-6xl mx-auto my-auto max-h-[90vh] overflow-y-auto z-10 p-6 md:p-8">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Manage Ticket Statuses</h3>
<button @click="$store.projectDetail.manageStatusesOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"><i class="fas fa-times text-xl"></i></button>
</div>
<!-- Create Status Form -->
<div class="rounded-lg bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 mb-4">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-3">New Status</h4>
<form method="post" action="{% url 'ticket-status-create' %}" class="grid grid-cols-12 gap-3 items-end">
{% csrf_token %}
<input type="hidden" name="project_id" value="{{ project.id }}">
<div class="col-span-3">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Name</label>
<input type="text" name="name" x-model="$store.projectDetail.createStatusName" required
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
</div>
<div class="col-span-2">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Color</label>
<div class="flex items-center gap-2">
<toolcool-color-picker popup-position="left" :color="$store.projectDetail.createStatusColor" @change="$store.projectDetail.createStatusColor = $event.detail.hex" class="shrink-0"></toolcool-color-picker>
<input type="text" name="color" x-model="$store.projectDetail.createStatusColor" required
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none uppercase font-mono">
</div>
</div>
<div class="col-span-2">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Order</label>
<input type="number" name="sort_order" x-model="$store.projectDetail.createStatusOrder" required
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
</div>
<div class="col-span-3 flex items-center gap-3 pb-1">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="is_completed" x-model="$store.projectDetail.createStatusCompleted" class="w-4 h-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500">
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">Completed</span>
</label>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors">Add</button>
</div>
</form>
</div>
<!-- Status List -->
<div class="rounded-xl bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Color</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Order</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Completed</th>
<th class="px-4 py-3 text-right text-xs font-medium 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 s in statuses %}
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30" x-data="{ editing: false }">
<td class="px-4 py-3 font-medium text-gray-900 dark:text-white">{{ s.name }}</td>
<td class="px-4 py-3">
<span class="inline-flex items-center gap-1.5 text-xs">
<span class="w-3 h-3 rounded-full inline-block" style="background-color:{{ s.color }}"></span>{{ s.color }}
</span>
</td>
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ s.sort_order }}</td>
<td class="px-4 py-3">
{% if s.is_completed %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400">Yes</span>
{% else %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400">No</span>
{% endif %}
</td>
<td class="px-4 py-3 text-right whitespace-nowrap">
<!-- Inline edit form -->
<form x-show="editing" x-cloak method="post" action="/ticket-statuses/{{ s.id }}/edit/" class="flex items-center gap-2">
{% csrf_token %}
<input type="text" name="name" x-model="$store.projectDetail.editStatusName" required
class="w-28 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-2 py-1 text-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
<div class="flex items-center gap-1">
<toolcool-color-picker popup-position="left" :color="$store.projectDetail.editStatusColor" @change="$store.projectDetail.editStatusColor = $event.detail.hex" class="shrink-0" style="height:28px;"></toolcool-color-picker>
<input type="text" name="color" x-model="$store.projectDetail.editStatusColor" required
class="w-20 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-2 py-1 text-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none uppercase font-mono">
</div>
<input type="number" name="sort_order" x-model="$store.projectDetail.editStatusOrder" required
class="w-16 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-2 py-1 text-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
<label class="flex items-center gap-1 text-xs whitespace-nowrap">
<input type="checkbox" name="is_completed" x-model="$store.projectDetail.editStatusCompleted" class="w-3 h-3 text-blue-600 rounded border-gray-300">
Done
</label>
<button type="submit" class="px-2 py-1 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700"><i class="fas fa-check"></i></button>
<button type="button" @click="editing = false" class="px-2 py-1 text-xs font-medium text-gray-600 bg-gray-100 dark:bg-gray-800 rounded hover:bg-gray-200"><i class="fas fa-times"></i></button>
</form>
<!-- Action buttons -->
<div x-show="!editing" class="flex items-center gap-1">
<button type="button" @click="$store.projectDetail.editStatusId='{{ s.id }}'; $store.projectDetail.editStatusName='{{ s.name|escapejs }}'; $store.projectDetail.editStatusColor='{{ s.color|escapejs }}'; $store.projectDetail.editStatusOrder='{{ s.sort_order }}'; $store.projectDetail.editStatusCompleted={{ s.is_completed|lower }}; editing = true" class="inline-flex items-center gap-1 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">
<i class="fas fa-pen"></i>
</button>
<form method="post" action="{% url 'ticket-status-delete' s.id %}" class="inline">
{% csrf_token %}
<input type="hidden" name="project_id" value="{{ project.id }}">
<button type="button" @click="confirmAction({title:'Delete Status', message:'Delete this status? This cannot be undone.', confirmText:'Delete', danger:true, onConfirm:function(){ $el.closest('form').submit() }})" class="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium text-red-600 dark:text-red-400 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors shadow-sm">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{% empty %}
<tr><td colspan="5" class="px-4 py-8 text-center text-sm text-gray-400">No statuses found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block extra_scripts %} {% block extra_scripts %}
@@ -571,6 +711,16 @@
notesSelectedIds: [], notesSelectedIds: [],
viewNoteOpen: false, viewNoteOpen: false,
viewNoteData: { id: '', title: '', content: '' }, viewNoteData: { id: '', title: '', content: '' },
manageStatusesOpen: false,
createStatusName: '',
createStatusColor: '#6B7280',
createStatusOrder: '0',
createStatusCompleted: false,
editStatusId: null,
editStatusName: '',
editStatusColor: '',
editStatusOrder: '0',
editStatusCompleted: false,
}); });
}); });
</script> </script>
+2 -13
View File
@@ -111,20 +111,9 @@
</div> </div>
{% if not project %} {% if not project %}
<div x-data="{ useDefaultStatuses: true }"> <div class="rounded-lg bg-blue-50 dark:bg-blue-900/10 border border-blue-200 dark:border-blue-800 p-4 text-sm text-blue-700 dark:text-blue-300">
<input type="hidden" name="use_default_statuses" :value="useDefaultStatuses ? '1' : '0'"> <i class="fas fa-info-circle mr-1"></i> After creating the project, you can define custom ticket statuses in the project settings.
<label class="flex items-start gap-3 cursor-pointer" @click.prevent="useDefaultStatuses = !useDefaultStatuses">
<div role="switch" class="mt-0.5 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="useDefaultStatuses ? 'background-color: var(--color-primary, #3b82f6)' : 'background-color: #d1d5db'">
<span class="pointer-events-none relative inline-block h-4 w-4 rounded-full bg-white shadow-sm ring-0 transition-transform duration-200 ease-in-out" :style="useDefaultStatuses ? 'transform: translateX(16px)' : 'transform: translateX(0)'"></span>
</div> </div>
<div>
<p class="text-sm font-semibold text-gray-900 dark:text-white">Use Default Ticket Statuses</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">Create standard Backlog, To Do, In Progress, Review, and Done statuses automatically</p>
</div>
</label>
</div>
{% else %}
<div></div>
{% endif %} {% endif %}
<div x-data="{ isPinned: {{ project.pinned_date|yesno:'true,false,false' }} }"> <div x-data="{ isPinned: {{ project.pinned_date|yesno:'true,false,false' }} }">
+2 -2
View File
@@ -17,7 +17,7 @@
<p class="text-gray-500 dark:text-gray-400 text-sm mt-1">{{ page_obj.paginator.count }} total projects</p> <p class="text-gray-500 dark:text-gray-400 text-sm mt-1">{{ page_obj.paginator.count }} total projects</p>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
{% if request.user.role != "member" %} {% 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> <a href="{% url 'project-create' %}" class="dk-btn dk-btn-primary text-sm"><i class="fas fa-plus"></i> New Project</a>
{% endif %} {% endif %}
<form class="flex gap-2" method="get"> <form class="flex gap-2" method="get">
@@ -215,7 +215,7 @@
<td colspan="20" class="text-center py-12 text-gray-400"> <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> <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> <p class="text-sm font-medium text-gray-500 dark:text-gray-400">No projects yet</p>
{% if request.user.role != "member" %} {% 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> <a href="{% url 'project-create' %}" class="text-primary text-sm mt-2 inline-block hover:underline">Create your first project</a>
{% endif %} {% endif %}
</td> </td>
@@ -19,12 +19,15 @@
<div class="flex gap-4 overflow-x-auto pb-4" style="min-height: 700px;"> <div class="flex gap-4 overflow-x-auto pb-4" style="min-height: 700px;">
{% for col in columns %} {% for col in columns %}
<div class="status-column shrink-0 w-[280px] max-w-[350px] bg-gray-50 dark:bg-gray-950/50 rounded-xl border border-gray-200 dark:border-gray-600 flex flex-col" data-status-id="{{ col.status.id }}" <div class="status-column shrink-0 w-[280px] max-w-[350px] bg-gray-50 dark:bg-gray-950/50 rounded-xl border border-gray-200 dark:border-gray-600 flex flex-col" data-status-id="{{ col.status.id }}" data-status-completed="{{ col.status.is_completed|lower }}"
@dragover.prevent="$el.classList.add('ring-2', 'ring-blue-500')" @dragover.prevent="$el.classList.add('ring-2', 'ring-blue-500')"
@dragleave="$el.classList.remove('ring-2', 'ring-blue-500')" @dragleave="$el.classList.remove('ring-2', 'ring-blue-500')"
@drop.prevent=" @drop.prevent="
$el.classList.remove('ring-2', 'ring-blue-500'); $el.classList.remove('ring-2', 'ring-blue-500');
if (!dragging) return; if (!dragging) return;
{% if not user_can_move_to_completed %}
if ($el.dataset.statusCompleted === 'true') return;
{% endif %}
const tid = dragging; const tid = dragging;
dragging = null; dragging = null;
const csrf = (document.cookie.split('; ').find(c => c.startsWith('csrftoken')) || '').split('=')[1]; const csrf = (document.cookie.split('; ').find(c => c.startsWith('csrftoken')) || '').split('=')[1];
@@ -66,12 +69,12 @@
<div class="flex items-center justify-between mt-2"> <div class="flex items-center justify-between mt-2">
<div class="flex -space-x-1.5"> <div class="flex -space-x-1.5">
{% for assignee in ticket.assignees.all|slice:":3" %} {% for assignee in ticket.assignees.all|slice:":3" %}
<div class="w-6 h-6 rounded-full border-2 border-white dark:border-gray-700 flex items-center justify-center text-[10px] font-bold text-white" style="background-color:{% cycle '#3B82F6' '#10B981' '#F59E0B' '#EF4444' '#8B5CF6' %}"> <div title="{{ assignee.get_full_name|default:assignee.username }}" class="w-6 h-6 rounded-full border-2 border-white dark:border-gray-700 flex items-center justify-center text-[10px] font-bold text-white" style="background-color:{% cycle '#3B82F6' '#10B981' '#F59E0B' '#EF4444' '#8B5CF6' %}">
{{ assignee.get_full_name|default:assignee.username|make_list|first|upper }} {{ assignee.get_full_name|default:assignee.username|make_list|first|upper }}
</div> </div>
{% endfor %} {% endfor %}
{% if ticket.assignees.count > 3 %} {% if ticket.assignees.count > 3 %}
<div class="w-6 h-6 rounded-full border-2 border-white dark:border-gray-700 bg-gray-200 dark:bg-gray-700 flex items-center justify-center text-[10px] font-bold text-gray-600 dark:text-gray-300">+{{ ticket.assignees.count|add:"-3" }}</div> <div title="View all assignees" class="w-6 h-6 rounded-full border-2 border-white dark:border-gray-700 bg-gray-200 dark:bg-gray-700 flex items-center justify-center text-[10px] font-bold text-gray-600 dark:text-gray-300">+{{ ticket.assignees.count|add:"-3" }}</div>
{% endif %} {% endif %}
</div> </div>
{% if ticket.due_date %} {% if ticket.due_date %}