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:
5 files changed
+314
-23
No files matched your search
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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' }} }">
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
Reference in new issue
Block a user