Files
project-managment/templates/projects/board.html
T
sankalp ce0c1349ca feat(templates/projects): add permissions modal and restricted mode logic
Implement permission configuration UI and enforce restricted movement rules
2026-06-28 19:35:30 +05:45

884 lines
56 KiB
HTML

{% extends "base.html" %}
{% block title %}Project Board{% endblock %}
{% block extra_head %}
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.default.min.css" rel="stylesheet">
{% include "components/rich_text_editor_assets.html" %}
<style>
.ts-control { border-radius: 0.5rem; border-color: #d1d5db; min-height: 38px; padding-top: 0.375rem; padding-bottom: 0.375rem; box-shadow: none; }
.dark .ts-control { background-color: #030712; border-color: #4b5563; color: #f3f4f6; }
.dark .ts-control input { color: #f3f4f6; }
.dark .ts-dropdown { background-color: #1f2937; border-color: #4b5563; color: #f3f4f6; }
.dark .ts-dropdown .option:hover { background-color: #374151; color: white; }
.dark .ts-dropdown .active { background-color: #374151; color: white; }
</style>
{% 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">Project Board</li>
</ol>
</nav>
{% endblock %}
{% block content %}
<div x-data="{
draggingTicket: null,
isTouchDevice: false,
touchStartX: 0,
touchStartY: 0,
scrollStartX: 0,
projectId: {{ selected_project.id|default:'null' }},
newTicketOpen: false,
exportOpen: false,
filterOpen: false,
filterOpen: false,
manageStatusesOpen: false,
permissionsOpen: false,
allowedMoveToStatusIds: {{ allowed_move_to_status_ids }},
createStatusName: '',
createStatusColor: '#6B7280',
createStatusOrder: '0',
createStatusCompleted: false,
editStatusId: null,
editStatusName: '',
editStatusColor: '',
editStatusOrder: '0',
editStatusCompleted: false,
ticketName: '',
ticketDescription: '',
ticketStatus: {{ first_status_id|default:'null' }},
ticketPriority: '',
ticketEpic: '',
ticketStartDate: '',
ticketDueDate: '',
ticketAssignees: [],
exportColumns: ['uuid', 'name', 'status', 'assignee', 'due_date', 'created_at'],
filterUserIds: {{ selected_user_ids|default:'[]' }},
moveTicketToStatus(ticketId, statusId) {
fetch('/dashboard/board/' + this.projectId + '/move/' + ticketId + '/', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}'},
body: JSON.stringify({status_id: statusId})
}).then(r => r.json()).then(data => {
if (data.success) window.location.reload();
});
},
submitExport() {
fetch('/dashboard/board/' + this.projectId + '/export/', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}'},
body: JSON.stringify({columns: this.exportColumns})
}).then(r => { if (r.ok) return r.blob(); throw new Error('Export failed'); })
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'board-export.xlsx';
document.body.appendChild(a); a.click(); a.remove();
window.URL.revokeObjectURL(url);
this.exportOpen = false;
}).catch(function(e) { showToast('Export failed: ' + e.message, 'error'); });
},
applyFilter() {
window.location.href = '?user_ids=' + this.filterUserIds.join(',');
},
clearFilter() {
window.location.href = '?';
},
toggleFilterUser(id) {
const idx = this.filterUserIds.indexOf(id);
idx > -1 ? this.filterUserIds.splice(idx, 1) : this.filterUserIds.push(id);
},
toggleExportColumn(col) {
const idx = this.exportColumns.indexOf(col);
idx > -1 ? this.exportColumns.splice(idx, 1) : this.exportColumns.push(col);
},
isColumnSelected(col) { return this.exportColumns.indexOf(col) > -1; },
init() {
this.$nextTick(() => {
this.removeAllEventListeners();
this.attachAllEventListeners();
this.setupTouchScrolling();
this.isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
});
},
setupTouchScrolling() {
const container = document.getElementById('board-container');
if (!container) return;
container.addEventListener('touchstart', (e) => {
this.touchStartX = e.touches[0].clientX;
this.touchStartY = e.touches[0].clientY;
this.scrollStartX = container.scrollLeft;
}, { passive: true });
container.addEventListener('touchmove', (e) => {
if (e.touches.length !== 1) return;
const dx = this.touchStartX - e.touches[0].clientX;
const dy = this.touchStartY - e.touches[0].clientY;
if (Math.abs(dx) > Math.abs(dy)) { e.preventDefault(); container.scrollLeft = this.scrollStartX + dx; }
}, { passive: false });
},
removeAllEventListeners() {
document.querySelectorAll('.ticket-card').forEach(t => {
t.removeAttribute('draggable');
const nt = t.cloneNode(true);
t.parentNode.replaceChild(nt, t);
});
document.querySelectorAll('.status-column').forEach(c => {
const nc = c.cloneNode(false);
while (c.firstChild) nc.appendChild(c.firstChild);
c.parentNode.replaceChild(nc, c);
});
},
attachAllEventListeners() {
{% if not can_move %}return;{% endif %}
document.querySelectorAll('.ticket-card').forEach(ticket => {
ticket.setAttribute('draggable', true);
ticket.addEventListener('dragstart', (e) => {
this.draggingTicket = ticket.getAttribute('data-ticket-id');
ticket.classList.add('opacity-50');
e.dataTransfer.effectAllowed = 'move';
});
ticket.addEventListener('dragend', () => {
ticket.classList.remove('opacity-50');
this.draggingTicket = null;
});
let longPressTimer, isDragging = false, originalColumn;
ticket.addEventListener('touchstart', (e) => {
if (isDragging) return;
longPressTimer = setTimeout(() => {
originalColumn = ticket.closest('.status-column');
this.draggingTicket = ticket.getAttribute('data-ticket-id');
ticket.classList.add('opacity-50', 'relative', 'z-30');
isDragging = true;
ticket.style.boxShadow = '0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -2px rgba(0,0,0,0.05)';
}, 500);
}, { passive: true });
ticket.addEventListener('touchmove', (e) => {
if (!isDragging) { clearTimeout(longPressTimer); return; }
const touch = e.touches[0];
document.querySelectorAll('.status-column').forEach(col => {
const rect = col.getBoundingClientRect();
if (touch.clientX >= rect.left && touch.clientX <= rect.right &&
touch.clientY >= rect.top && touch.clientY <= rect.bottom)
col.classList.add('bg-blue-50', 'dark:bg-blue-900/20');
else
col.classList.remove('bg-blue-50', 'dark:bg-blue-900/20');
});
});
ticket.addEventListener('touchend', (e) => {
clearTimeout(longPressTimer);
if (!isDragging) return;
isDragging = false;
ticket.classList.remove('opacity-50', 'relative', 'z-30');
ticket.style.boxShadow = '';
const touch = e.changedTouches[0];
let targetColumn = null;
document.querySelectorAll('.status-column').forEach(col => {
const rect = col.getBoundingClientRect();
if (touch.clientX >= rect.left && touch.clientX <= rect.right &&
touch.clientY >= rect.top && touch.clientY <= rect.bottom) targetColumn = col;
col.classList.remove('bg-blue-50', 'dark:bg-blue-900/20');
});
{% if can_move %}
if (targetColumn && targetColumn.getAttribute('data-status-id') && !this.allowedMoveToStatusIds.includes(parseInt(targetColumn.getAttribute('data-status-id')))) targetColumn = null;
{% endif %}
if (targetColumn && targetColumn !== originalColumn)
this.moveTicketToStatus(this.draggingTicket, targetColumn.getAttribute('data-status-id'));
this.draggingTicket = null;
});
ticket.addEventListener('touchcancel', () => {
clearTimeout(longPressTimer);
if (!isDragging) return;
isDragging = false;
ticket.classList.remove('opacity-50', 'relative', 'z-30');
ticket.style.boxShadow = '';
this.draggingTicket = null;
document.querySelectorAll('.status-column').forEach(col => col.classList.remove('bg-blue-50', 'dark:bg-blue-900/20'));
});
});
document.querySelectorAll('.status-column').forEach(col => {
col.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; col.classList.add('bg-blue-50', 'dark:bg-blue-900/20'); });
col.addEventListener('dragleave', () => { col.classList.remove('bg-blue-50', 'dark:bg-blue-900/20'); });
col.addEventListener('drop', (e) => {
e.preventDefault();
col.classList.remove('bg-blue-50', 'dark:bg-blue-900/20');
{% if can_move %}
if (col.getAttribute('data-status-id') && !this.allowedMoveToStatusIds.includes(parseInt(col.getAttribute('data-status-id')))) return;
{% endif %}
if (this.draggingTicket) {
this.moveTicketToStatus(this.draggingTicket, col.getAttribute('data-status-id'));
this.draggingTicket = null;
}
});
});
}
}"
x-init="init()"
class="space-y-6"
>
{% if not selected_project %}
<div class="mb-6 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 class="text-2xl font-bold">Project Board</h1>
<p class="text-gray-500 dark:text-gray-400 text-sm mt-1">Drag and drop tickets across status columns</p>
</div>
</div>
<!-- Project Selector -->
<div class="rounded-xl bg-white dark:bg-gray-950 shadow-sm ring-1 ring-gray-950/5 dark:ring-white/10 overflow-hidden">
<div class="px-6 py-5">
<div class="mb-5">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Select Project</h2>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">Choose a project to view its board</p>
</div>
<form method="get" class="mb-4">
<div class="relative">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<i class="fas fa-search text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" name="search" value="{{ search }}" placeholder="Search projects by name or prefix..."
class="block w-full pl-10 pr-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-950 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-600 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none">
</div>
</form>
{% if not projects %}
<div class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400">
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-1">No Projects Available</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">You don't have access to any projects yet.</p>
</div>
{% elif search and not projects %}
<div class="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400">
<i class="fas fa-search text-4xl mb-3 text-gray-400 dark:text-gray-500"></i>
<h3 class="text-base font-medium text-gray-900 dark:text-white mb-1">No Projects Found</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Try adjusting your search terms</p>
</div>
{% else %}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{% for p in projects %}
<a href="{% url 'project-board-project' p.id %}"
class="relative p-4 bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-600 rounded-lg hover:shadow-md transition-all text-left overflow-hidden block"
style="border-left: 4px solid {{ p.color|default:'#6B7280' }};">
{% if p.pinned_date %}
<div class="absolute top-2 right-2">
<div class="flex items-center justify-center w-6 h-6 rounded-full shadow-sm" style="background-color:{{ p.color|default:'#6B7280' }};" title="Pinned Project">
<i class="fas fa-thumbtack text-white text-xs"></i>
</div>
</div>
{% endif %}
<div class="inline-flex px-2.5 py-1 rounded text-xs font-semibold mb-3 text-white"
style="background-color:{{ p.color|default:'#6B7280' }};">{{ p.ticket_prefix }}</div>
<h3 class="font-semibold text-base text-gray-900 dark:text-white line-clamp-2">{{ p.name }}</h3>
</a>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% else %}
<div class="mb-6 flex flex-col sm:flex-row sm:items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-bold">Project Board</h1>
<p class="text-gray-500 dark:text-gray-400 text-sm mt-1">{{ selected_project.name }}</p>
</div>
<div class="flex flex-wrap items-center gap-2">
<button @click="window.location.reload()" class="dk-btn dk-btn-ghost text-sm text-gray-700 dark:text-gray-300">
<i class="fas fa-sync"></i> Refresh
</button>
<button @click="exportOpen = true" class="dk-btn dk-btn-ghost text-sm text-emerald-600">
<i class="fas fa-download"></i> Export
</button>
<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 %}
</button>
{% if is_admin %}
<button @click="permissionsOpen = true" class="dk-btn dk-btn-ghost text-sm text-orange-600">
<i class="fas fa-shield-halved"></i> Permissions
</button>
<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">
<i class="fas fa-plus"></i> New Ticket
</button>
</div>
</div>
<!-- Project Switcher -->
<div class="mb-4" x-data="{ open: false }">
<div class="relative">
<button @click="open = !open" @click.away="open = false"
class="inline-flex items-center gap-2 px-4 py-2.5 text-sm font-medium text-gray-900 dark:text-white bg-white dark:bg-gray-950 border-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
style="border-color: {{ selected_project.color|default:'#D1D5DB' }};">
<span class="px-2 py-0.5 rounded text-xs font-semibold text-white" style="background-color:{{ selected_project.color|default:'#6B7280' }};">{{ selected_project.ticket_prefix }}</span>
<span>{{ selected_project.name }}</span>
<i class="fas fa-chevron-down w-4 h-4 transition-transform" :class="{ 'rotate-180': open }"></i>
</button>
<div x-cloak x-show="open" x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75" x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute top-full left-0 mt-2 w-80 bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg z-50 max-h-96 overflow-y-auto">
<div class="p-2">
<div class="px-3 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">Switch Project</div>
{% for p in projects %}
<a href="{% url 'project-board-project' p.id %}"
class="w-full flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors text-left {% if p.id == selected_project.id %}bg-gray-50 dark:bg-gray-800{% endif %}">
<span class="px-2 py-0.5 rounded text-xs font-semibold text-white flex-shrink-0" style="background-color:{{ p.color|default:'#6B7280' }};">{{ p.ticket_prefix }}</span>
<div class="flex-1 min-w-0 text-sm font-medium text-gray-900 dark:text-white truncate">{{ p.name }}</div>
{% if p.id == selected_project.id %}<i class="fas fa-check flex-shrink-0" style="color:{{ p.color|default:'#3B82F6' }};"></i>{% endif %}
</a>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- Mobile swipe hint -->
<div class="md:hidden flex justify-center mb-2 text-xs text-gray-500 dark:text-gray-400 items-center gap-1">
<i class="fas fa-arrow-left"></i><span>Swipe horizontally to view all columns</span><i class="fas fa-arrow-right"></i>
</div>
<!-- View Only Mode Indicator -->
{% if not can_move %}
<div class="flex justify-center mb-4">
<div class="inline-flex items-center gap-2 px-4 py-2 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg text-amber-800 dark:text-amber-200">
<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>
{% elif can_move and allowed_move_to_status_ids|length < columns|length %}
<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 only move tickets to certain statuses</span>
</div>
</div>
{% endif %}
<!-- Board Columns -->
<div id="board-container" class="relative overflow-x-auto pb-6 {% if not can_move %}view-only-mode{% endif %}">
<div class="inline-flex gap-4 pb-2 min-w-full">
{% 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]"
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"
style="background-color:{{ col.status.color|default:'#f3f4f6' }};">
<div class="flex items-center justify-between">
<h3 class="font-medium flex items-center gap-2" style="color:white;text-shadow:0px 0px 1px rgba(0,0,0,0.5);">
<span>{{ col.status.name }}</span>
<span class="text-sm opacity-80">{{ col.count }}</span>
{% if col.status.is_completed %}
<div class="flex items-center justify-center w-6 h-6 bg-green-500 rounded-full border-2 border-white dark:border-gray-700 shadow-lg" title="Completed Status">
<i class="fas fa-check text-white text-xs font-bold"></i>
</div>
{% endif %}
</h3>
<div class="relative" x-data="{ open: false }">
<button @click="open = !open" @click.away="open = false"
class="p-1 rounded hover:bg-black hover:bg-opacity-20 transition-colors" style="color:white;">
<i class="fas fa-ellipsis-v"></i>
</button>
<div x-cloak x-show="open"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="absolute top-8 left-0 w-52 bg-white dark:bg-gray-950 rounded-lg shadow-lg border border-gray-200 dark:border-gray-600 z-50"
style="transform:translateX(-100%);">
<div class="p-2">
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 dark:border-gray-600">
<span class="text-sm font-medium text-gray-900 dark:text-white">Sort list</span>
<button @click="open = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"><i class="fas fa-times"></i></button>
</div>
<div class="py-1">
<a href="?sort=created_at_desc" class="w-full text-left block px-3 py-2 text-sm text-gray-700 dark:text-white rounded hover:bg-gray-50 dark:hover:bg-gray-800 {% if sort == 'created_at_desc' %}bg-gray-50 dark:bg-gray-800{% endif %}">Date created (newest first)</a>
<a href="?sort=created_at_asc" class="w-full text-left block px-3 py-2 text-sm text-gray-700 dark:text-white rounded hover:bg-gray-50 dark:hover:bg-gray-800 {% if sort == 'created_at_asc' %}bg-gray-50 dark:bg-gray-800{% endif %}">Date created (oldest first)</a>
<a href="?sort=name_asc" class="w-full text-left block px-3 py-2 text-sm text-gray-700 dark:text-white rounded hover:bg-gray-50 dark:hover:bg-gray-800 {% if sort == 'name_asc' %}bg-gray-50 dark:bg-gray-800{% endif %}">Card name (alphabetically)</a>
<a href="?sort=due_date_asc" class="w-full text-left block px-3 py-2 text-sm text-gray-700 dark:text-white rounded hover:bg-gray-50 dark:hover:bg-gray-800 {% if sort == 'due_date_asc' %}bg-gray-50 dark:bg-gray-800{% endif %}">Due date</a>
<a href="?sort=priority" class="w-full text-left block px-3 py-2 text-sm text-gray-700 dark:text-white rounded hover:bg-gray-50 dark:hover:bg-gray-800 {% if sort == 'priority' %}bg-gray-50 dark:bg-gray-800{% endif %}">Priority</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="p-3 flex flex-col gap-3 flex-1 overflow-y-auto" style="max-height:calc(100% - 60px);">
{% for ticket in col.tickets %}
<div class="ticket-card bg-white dark:bg-gray-950 p-3 rounded-lg shadow-sm border border-gray-200 dark:border-gray-600 {% if can_move %}cursor-move{% endif %}"
data-ticket-id="{{ ticket.id }}">
<div class="flex justify-between items-center mb-2">
<span class="text-xs font-mono text-gray-500 dark:text-gray-400 px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 rounded truncate max-w-[120px] sm:max-w-none">{{ ticket.uuid }}</span>
<div class="flex items-center gap-1">
{% if ticket.priority %}<span class="text-xs px-1.5 py-0.5 rounded text-white font-medium" style="background-color:{{ ticket.priority.color }};">{{ ticket.priority.name }}</span>{% endif %}
{% if ticket.due_date %}<span class="text-xs px-1.5 py-0.5 rounded {% if ticket.due_date < today %}bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300{% else %}bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300{% endif %}">{{ ticket.due_date|date:"M d" }}</span>{% endif %}
</div>
</div>
<h4 class="font-medium text-gray-900 dark:text-white mb-2 text-sm">{{ ticket.name }}</h4>
{% if ticket.description %}<p class="text-sm text-gray-500 dark:text-gray-400 mb-3 line-clamp-2">{{ ticket.description|striptags|truncatechars:100 }}</p>{% endif %}
<div class="flex justify-between items-center mt-2">
<div class="flex flex-wrap gap-1 max-w-[180px]">
{% if ticket.assignees.all %}
{% 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">
<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 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>
{% endfor %}
{% 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 %}
<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>
</div>
{% endif %}
</div>
<a href="{% url 'ticket-detail' ticket.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>
</div>
</div>
{% empty %}
<div class="flex items-center justify-center h-24 text-gray-500 dark:text-gray-400 text-sm italic border border-dashed border-gray-300 dark:border-gray-600 rounded-lg">No tickets</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% if not columns %}
<div class="w-full flex items-center justify-center h-40 text-gray-500 dark:text-gray-400">No status columns found for this project</div>
{% endif %}
</div>
</div>
<!-- New Ticket Modal -->
<div x-cloak x-show="newTicketOpen" class="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
@keydown.escape.window="newTicketOpen = false">
<div class="fixed inset-0 bg-black/50" @click="newTicketOpen = 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-4xl 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-6">
<h3 class="text-xl font-bold text-gray-900 dark:text-white">New Ticket</h3>
<button @click="newTicketOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"><i class="fas fa-times text-xl"></i></button>
</div>
<form id="new-ticket-form" method="post" action="{% url 'board-create-ticket' selected_project.id %}">
{% csrf_token %}
<div class="space-y-6">
<!-- Row 1: Project, Status, Priority -->
<div class="grid grid-cols-3 gap-5">
<div>
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">Project</label>
<select disabled class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 px-3 py-2 text-sm text-gray-700 outline-none">
<option>{{ selected_project.name }}</option>
</select>
<input type="hidden" name="project" value="{{ selected_project.id }}">
</div>
<div>
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">Status <span class="text-red-500">*</span></label>
<select name="status" required x-model="ticketStatus"
class="w-full text-sm outline-none">
{% for s in columns %}<option value="{{ s.status.id }}">{{ s.status.name }}</option>{% endfor %}
</select>
</div>
<div>
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">Priority</label>
<select name="priority" x-model="ticketPriority"
class="w-full text-sm outline-none">
<option value="">Select an option</option>
{% for p in priorities %}<option value="{{ p.id }}">{{ p.name }}</option>{% endfor %}
</select>
</div>
</div>
<!-- Row 2: Epic, Ticket Name -->
<div class="grid grid-cols-3 gap-5">
<div>
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">Epic</label>
<select name="epic" x-model="ticketEpic"
class="w-full text-sm outline-none">
<option value="">Select an option</option>
{% for e in epics %}<option value="{{ e.id }}">{{ e.name }}</option>{% endfor %}
</select>
</div>
<div>
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">Ticket Name <span class="text-red-500">*</span></label>
<input type="text" name="name" required x-model="ticketName"
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 transition-colors">
</div>
</div>
<div>
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">Description</label>
{% include "components/rich_text_editor.html" with input_name="description" alpine_model="ticketDescription" %}
</div>
<!-- Row 4: Assigned to, Start Date, Due Date -->
<div class="grid grid-cols-3 gap-5">
<div>
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">Assigned to</label>
<select name="assignees" multiple x-ref="assigneesSelect"
class="w-full text-sm outline-none">
{% for member in project_members %}
<option value="{{ member.user.id }}">{{ member.user.get_full_name|default:member.user.username }}</option>
{% endfor %}
</select>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1.5">Select multiple users to assign this ticket to. Only project members can be assigned.</p>
</div>
<div>
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">Start Date</label>
<input type="date" name="start_date" x-model="ticketStartDate" @click="$el.showPicker()"
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 transition-colors">
</div>
<div>
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">Due Date</label>
<input type="date" name="due_date" x-model="ticketDueDate" @click="$el.showPicker()"
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 transition-colors">
</div>
</div>
<div class="flex items-center gap-3">
<button type="submit" class="dk-btn dk-btn-primary">
Submit
</button>
<button type="button" @click="newTicketOpen = false"
class="dk-btn dk-btn-ghost">
Cancel
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Export to Excel Modal -->
<div x-cloak x-show="exportOpen" class="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
@keydown.escape.window="exportOpen = false">
<div class="fixed inset-0 bg-black/50" @click="exportOpen = 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-md mx-4 z-10">
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-600">
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Export to Excel</h3>
<p class="text-xs text-gray-500 dark:text-gray-400">Select columns to include in the export</p>
</div>
<button @click="exportOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"><i class="fas fa-times"></i></button>
</div>
<div class="px-6 py-4">
<div class="grid grid-cols-2 gap-2">
<label class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"> <input type="checkbox" :checked="isColumnSelected('uuid')" @change="toggleExportColumn('uuid')" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"> <span class="text-sm text-gray-700 dark:text-gray-300">Ticket ID</span> </label> <label class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"> <input type="checkbox" :checked="isColumnSelected('name')" @change="toggleExportColumn('name')" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"> <span class="text-sm text-gray-700 dark:text-gray-300">Title</span> </label> <label class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"> <input type="checkbox" :checked="isColumnSelected('description')" @change="toggleExportColumn('description')" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"> <span class="text-sm text-gray-700 dark:text-gray-300">Description</span> </label> <label class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"> <input type="checkbox" :checked="isColumnSelected('status')" @change="toggleExportColumn('status')" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"> <span class="text-sm text-gray-700 dark:text-gray-300">Status</span> </label> <label class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"> <input type="checkbox" :checked="isColumnSelected('assignee')" @change="toggleExportColumn('assignee')" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"> <span class="text-sm text-gray-700 dark:text-gray-300">Assignee</span> </label> <label class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"> <input type="checkbox" :checked="isColumnSelected('project')" @change="toggleExportColumn('project')" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"> <span class="text-sm text-gray-700 dark:text-gray-300">Project</span> </label> <label class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"> <input type="checkbox" :checked="isColumnSelected('epic')" @change="toggleExportColumn('epic')" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"> <span class="text-sm text-gray-700 dark:text-gray-300">Epic</span> </label> <label class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"> <input type="checkbox" :checked="isColumnSelected('start_date')" @change="toggleExportColumn('start_date')" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"> <span class="text-sm text-gray-700 dark:text-gray-300">Start Date</span> </label> <label class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"> <input type="checkbox" :checked="isColumnSelected('due_date')" @change="toggleExportColumn('due_date')" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"> <span class="text-sm text-gray-700 dark:text-gray-300">Due Date</span> </label> <label class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"> <input type="checkbox" :checked="isColumnSelected('created_at')" @change="toggleExportColumn('created_at')" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"> <span class="text-sm text-gray-700 dark:text-gray-300">Created At</span> </label> <label class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"> <input type="checkbox" :checked="isColumnSelected('updated_at')" @change="toggleExportColumn('updated_at')" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"> <span class="text-sm text-gray-700 dark:text-gray-300">Updated At</span> </label>
</div>
</div>
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200 dark:border-gray-600">
<button type="button" @click="exportOpen = false"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors">Cancel</button>
<button @click="submitExport()"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white rounded-lg bg-green-600 hover:bg-green-500">
<i class="fas fa-download"></i> Download
</button>
</div>
</div>
</div>
<!-- Filter by User Modal -->
<div x-cloak x-show="filterOpen" class="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
@keydown.escape.window="filterOpen = false">
<div class="fixed inset-0 bg-black/50" @click="filterOpen = 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-md mx-4 z-10">
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-600">
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Filter by User</h3>
<p class="text-xs text-gray-500 dark:text-gray-400">Select users to filter tickets</p>
</div>
<button @click="filterOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"><i class="fas fa-times"></i></button>
</div>
<div class="px-6 py-4">
<div class="space-y-1 max-h-64 overflow-y-auto">
{% for u in project_users %}
<label class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer">
<input type="checkbox" :checked="filterUserIds.indexOf({{ u.id }}) > -1"
@change="toggleFilterUser({{ u.id }})"
class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700 dark:text-gray-300">{{ u.full_name }}</span>
</label>
{% empty %}<p class="text-sm text-gray-400 py-4 text-center">No users with tickets in this project</p>{% endfor %}
</div>
</div>
<div class="flex items-center justify-between gap-3 px-6 py-4 border-t border-gray-200 dark:border-gray-600">
<button @click="clearFilter()"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors">Clear</button>
<div class="flex items-center gap-3">
<button type="button" @click="filterOpen = false"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors">Cancel</button>
<button @click="applyFilter()"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white rounded-lg"
style="background-color:var(--color-primary,#3b82f6);">
<i class="fas fa-check"></i> Apply
</button>
</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>
<!-- Board Permissions Modal -->
<div x-cloak x-show="permissionsOpen" class="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
@keydown.escape.window="permissionsOpen = false">
<div class="fixed inset-0 bg-black/50" @click="permissionsOpen = 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">Board Permissions</h3>
<button @click="permissionsOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"><i class="fas fa-times text-xl"></i></button>
</div>
<!-- Add Permission 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">Add / Update Permission</h4>
<form method="post" action="{% url 'board-permissions' selected_project.id %}">
{% csrf_token %}
<div class="grid grid-cols-12 gap-3 items-start">
<div class="col-span-3">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Role</label>
<select name="role" 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">
<option value="">Select a role</option>
{% for role_key, role_label in role_choices %}
{% if role_key not in admin_roles %}
<option value="{{ role_key }}">{{ role_label }}</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="col-span-7">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Can move to</label>
<div class="grid grid-cols-3 gap-x-4 gap-y-1.5">
{% for col in columns %}
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="move_to_statuses" value="{{ col.status.id }}"
class="w-3.5 h-3.5 text-blue-600 rounded border-gray-300 focus:ring-blue-500">
<span class="w-2.5 h-2.5 rounded-full inline-block flex-shrink-0" style="background-color:{{ col.status.color }}"></span>
<span class="text-xs text-gray-700 dark:text-gray-300 truncate">{{ col.status.name }}</span>
</label>
{% endfor %}
</div>
</div>
<div class="col-span-2 pt-5">
<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 w-full">Save</button>
</div>
</div>
</form>
</div>
<!-- Permissions 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">Role</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Can move to</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 perm in permissions %}
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30">
<td class="px-4 py-3 font-medium text-gray-900 dark:text-white">{{ perm.get_role_display }}</td>
<td class="px-4 py-3">
<div class="flex flex-wrap gap-1.5">
{% for status in perm.move_to_statuses.all %}
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium" style="background-color:{{ status.color }}20;color:{{ status.color }}">
<span class="w-1.5 h-1.5 rounded-full" style="background-color:{{ status.color }}"></span>
{{ status.name }}
</span>
{% empty %}
<span class="text-xs text-gray-400 italic">None</span>
{% endfor %}
</div>
</td>
<td class="px-4 py-3 text-right">
<form method="post" action="{% url 'board-permission-delete' selected_project.id perm.id %}" class="inline">
{% csrf_token %}
<button type="button" @click="confirmAction({title:'Delete Permission', message:'Delete permission for <strong>' + '{{ perm.get_role_display|escapejs }}' + '</strong>?', 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>
</td>
</tr>
{% empty %}
<tr><td colspan="3" class="px-4 py-8 text-center text-sm text-gray-400">No permissions configured. All non-admin roles are restricted by default.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-4"><i class="fas fa-info-circle"></i> Superadmin and manager roles always have full access. Select the statuses a developer role can move tickets to.</p>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_scripts %}
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll('#new-ticket-form select:not([disabled])').forEach((el) => {
let plugins = [];
if (el.hasAttribute('multiple')) {
plugins.push('remove_button');
}
new TomSelect(el, {
plugins: plugins,
create: false,
maxOptions: 50
});
});
});
</script>
{% endblock %}