Files
project-managment/templates/projects/detail.html
T
sankalp 2f1df8ab03 feat(templates/projects): add automation UI and modal for board automation rules
Enable management of automation rules with UI buttons, modal, and status condition handling
2026-06-29 07:50:09 +05:45

844 lines
65 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}{{ project.name }}{% 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><span class="text-gray-300 dark:text-gray-600">/</span></li>
<li><a href="{% url 'projects-list' %}" class="hover:text-gray-700 dark:hover:text-gray-300">Projects</a></li>
<li><span class="text-gray-300 dark:text-gray-600">/</span></li>
<li class="text-gray-900 dark:text-white font-medium" aria-current="page">{{ project.name }}</li>
</ol>
</nav>
{% endblock %}
{% block content %}
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="inline-flex items-center px-2.5 py-0.5 rounded text-xs font-semibold text-white" style="background-color:{{ project.color|default:'#3B82F6' }}">{{ project.ticket_prefix }}</span>
<div>
<h1 class="text-2xl font-bold">{{ project.name }}</h1>
</div>
</div>
<div class="flex items-center gap-2">
<a href="{% url 'projects-list' %}" class="dk-btn dk-btn-ghost text-sm"><i class="fas fa-arrow-left mr-1"></i> Back</a>
<a href="{% url 'project-board-project' project.id %}" class="dk-btn dk-btn-ghost text-sm"><i class="fas fa-columns"></i> Board</a>
<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="$store.projectDetail.externalModalOpen = true" class="dk-btn dk-btn-ghost text-sm"><i class="fas fa-globe"></i> External</button>
{% if is_admin %}
<button @click="$store.projectDetail.permissionsOpen = true" class="dk-btn dk-btn-ghost text-sm text-orange-600">
<i class="fas fa-shield-halved"></i> Permissions
</button>
<button @click="$store.projectDetail.automationOpen = true" class="dk-btn dk-btn-ghost text-sm text-cyan-600">
<i class="fas fa-robot"></i> Automation
</button>
<button @click="$store.projectDetail.manageStatusesOpen = true" class="dk-btn dk-btn-ghost text-sm text-purple-600">
<i class="fas fa-cog"></i> Statuses
</button>
{% endif %}
<a href="{% url 'project-edit' project.id %}" class="dk-btn dk-btn-primary text-sm"><i class="fas fa-pen"></i> Edit</a>
</div>
</div>
<div class="max-w-full space-y-6">
{# Row 1: Left col (Project Info + Timestamps) | Right col (Statistics) #}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
{# LEFT: Project Information + Timestamps stacked #}
<div class="space-y-6">
{# Project Information Card #}
<div class="rounded-xl bg-white dark:bg-gray-950 shadow-sm ring-1 ring-gray-950/5 dark:ring-white/10 p-6 space-y-6">
<h3 class="text-base font-bold text-gray-900 dark:text-white border-b border-gray-100 dark:border-gray-800 pb-4">Project Information</h3>
<div class="grid grid-cols-2 gap-6">
<!-- Project Name -->
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1.5">Project Name</h4>
<p class="text-lg font-bold text-gray-900 dark:text-white">{{ project.name }}</p>
</div>
<!-- Ticket Prefix -->
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1.5">Ticket Prefix</h4>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400">
{{ project.ticket_prefix }}
</span>
</div>
</div>
<!-- Description -->
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1.5">Description</h4>
{% if project.description %}
<div class="text-sm text-gray-800 dark:text-gray-300 leading-relaxed prose prose-sm dark:prose-invert max-w-none">{{ project.description|safe }}</div>
{% else %}
<p class="text-sm text-gray-400 dark:text-gray-600 italic">No description provided.</p>
{% endif %}
</div>
<div class="grid grid-cols-2 gap-6">
<!-- Start Date -->
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1.5">Start Date</h4>
<p class="text-sm text-gray-800 dark:text-gray-300">{{ project.start_date|date:"d/m/Y"|default:"—" }}</p>
</div>
<!-- End Date -->
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1.5">End Date</h4>
<p class="text-sm text-gray-800 dark:text-gray-300">{{ project.end_date|date:"d/m/Y"|default:"—" }}</p>
</div>
</div>
<div class="grid grid-cols-2 gap-6">
<!-- Remaining Days -->
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1.5">Remaining Days</h4>
{% if remaining_days is not None %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold {% if remaining_days == 1 %}bg-red-50 text-red-600 dark:bg-red-900/30 dark:text-red-400{% elif remaining_days <= 7 %}bg-amber-50 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400{% else %}bg-green-50 text-green-600 dark:bg-green-900/30 dark:text-green-400{% endif %}">
{{ remaining_days }} days
</span>
{% else %}
<span class="text-sm text-gray-400">—</span>
{% endif %}
</div>
<!-- Pinned Status -->
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1.5">Pinned Status</h4>
{% if project.pinned_date %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-amber-50 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400">Pinned</span>
{% else %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400">Not pinned</span>
{% endif %}
</div>
</div>
</div>
{# Timestamps Card #}
<div x-data="{ open: true }" class="rounded-xl bg-white dark:bg-gray-950 shadow-sm ring-1 ring-gray-950/5 dark:ring-white/10">
<button @click="open = !open" class="w-full px-6 py-4 flex items-center justify-between border-b border-gray-100 dark:border-gray-800 text-left focus:outline-none">
<h3 class="text-base font-bold text-gray-900 dark:text-white">Timestamps</h3>
<i class="fas fa-chevron-up text-gray-400 transition-transform duration-200" :class="{'rotate-180': !open}"></i>
</button>
<div x-show="open" x-collapse class="px-6 py-6">
<div class="grid grid-cols-2 gap-6">
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1.5">Created At</h4>
<p class="text-sm text-gray-800 dark:text-gray-300">{{ project.created_at|date:"d/m/Y H:i" }}</p>
</div>
<div>
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1.5">Last Updated</h4>
<p class="text-sm text-gray-800 dark:text-gray-300">{{ project.updated_at|date:"d/m/Y H:i" }}</p>
</div>
</div>
</div>
</div>
</div>
{# RIGHT: Project Statistics Card #}
<div class="rounded-xl bg-white dark:bg-gray-950 shadow-sm ring-1 ring-gray-950/5 dark:ring-white/10 h-fit">
<div class="px-6 py-4 border-b border-gray-100 dark:border-gray-800">
<h3 class="text-base font-bold text-gray-900 dark:text-white">Project Statistics</h3>
</div>
<div class="px-6 py-5 flex items-start justify-between gap-8 overflow-x-auto whitespace-nowrap">
<div class="flex flex-col gap-2">
<span class="text-sm font-semibold text-gray-900 dark:text-white">Total Members</span>
<div>
<span class="inline-flex items-center justify-center px-2 py-0.5 rounded text-xs font-bold bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400">{{ members_count }}</span>
</div>
</div>
<div class="flex flex-col gap-2">
<span class="text-sm font-semibold text-gray-900 dark:text-white">Total Tickets</span>
<div>
<span class="inline-flex items-center justify-center px-2 py-0.5 rounded text-xs font-bold bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400">{{ tickets_count_total }}</span>
</div>
</div>
<div class="flex flex-col gap-2">
<span class="text-sm font-semibold text-gray-900 dark:text-white">Total Epics</span>
<div>
<span class="inline-flex items-center justify-center px-2 py-0.5 rounded text-xs font-bold bg-amber-50 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400">{{ epics_count }}</span>
</div>
</div>
<div class="flex flex-col gap-2">
<span class="text-sm font-semibold text-gray-900 dark:text-white">Ticket Statuses</span>
<div>
<span class="inline-flex items-center justify-center px-2 py-0.5 rounded text-xs font-bold bg-green-50 text-green-600 dark:bg-green-900/30 dark:text-green-400">{{ statuses_count }}</span>
</div>
</div>
</div>
</div>
</div>
{% include "components/project_tabs.html" with active_tab_var="$store.projectDetail.activeTab" %}
<!-- STATUSES TAB -->
<div x-show="$store.projectDetail.activeTab === 'statuses'" 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-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>
<div class="flex items-center gap-3 flex-wrap">
{% if is_admin %}
<button type="button" @click="$store.projectDetail.permissionsOpen = true" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-orange-600 bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg hover:bg-orange-100 dark:hover:bg-orange-900/30 transition-colors">
<i class="fas fa-shield-halved"></i> Permissions
</button>
<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">
<div class="relative">
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs"></i>
<input type="text" name="statuses_search" value="{{ statuses_search }}" placeholder="Search..." class="pl-8 pr-3 py-1.5 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 w-44">
</div>
{% if statuses_search %}<a href="{% url 'project-detail' project.id %}" class="text-xs text-gray-500 hover:text-gray-700">Clear</a>{% endif %}
</form>
<div x-data="{ open: false }" class="relative">
<button @click="open = !open" class="text-xs border border-gray-200 dark:border-gray-600 rounded px-2.5 py-1.5 bg-white dark:bg-gray-900 hover:bg-gray-50 dark:hover:bg-gray-800 flex items-center gap-1.5"><i class="fas fa-columns"></i> Columns</button>
<div x-cloak x-show="open" @click.away="open = false" class="absolute right-0 mt-1 w-48 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg z-20 py-1">
<form method="get">
{% for col in all_statuses_columns %}
<label class="flex items-center gap-2 px-3 py-1.5 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800/50 cursor-pointer">
<input type="checkbox" name="statuses_columns" value="{{ col }}" {% if col in statuses_columns %}checked{% endif %} class="rounded border-gray-300 text-blue-600 focus:ring-blue-500" @change="$event.target.closest('form').submit()"> {{ col|title }}
</label>
{% endfor %}
</form>
</div>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
{% if "name" in statuses_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</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 "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>
</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">
{% if "name" in statuses_columns %}<td class="px-4 py-3 font-medium text-gray-900 dark:text-white">{{ s.name }}</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 "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>
{% empty %}
<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 %}
</tbody>
</table>
</div>
{% if statuses_paginator.paginator.num_pages > 1 %}
<div class="px-6 py-3 border-t border-gray-100 dark:border-gray-700 flex items-center justify-between text-xs text-gray-500">
<span>{{ statuses.start_index }}–{{ statuses.end_index }} of {{ statuses.paginator.count }}</span>
<div class="flex items-center gap-1">
{% if statuses.has_previous %}<a href="?statuses_page={{ statuses.previous_page_number }}&statuses_search={{ statuses_search }}" class="px-2 py-1 border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">&lsaquo;</a>{% endif %}
<span class="px-2">{{ statuses.number }}/{{ statuses.paginator.num_pages }}</span>
{% if statuses.has_next %}<a href="?statuses_page={{ statuses.next_page_number }}&statuses_search={{ statuses_search }}" class="px-2 py-1 border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">&rsaquo;</a>{% endif %}
</div>
</div>
{% endif %}
</div>
<!-- MEMBERS TAB -->
<div x-show="$store.projectDetail.activeTab === 'members'" 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-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">Members</h3>
<div class="flex items-center gap-3 flex-wrap">
<form method="get" class="flex items-center gap-2">
<div class="relative">
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs"></i>
<input type="text" name="members_search" value="{{ members_search }}" placeholder="Search..." class="pl-8 pr-3 py-1.5 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 w-44">
</div>
{% if members_search %}<a href="{% url 'project-detail' project.id %}" class="text-xs text-gray-500 hover:text-gray-700">Clear</a>{% endif %}
</form>
<div x-data="{ open: false }" class="relative">
<button @click="open = !open" class="text-xs border border-gray-200 dark:border-gray-600 rounded px-2.5 py-1.5 bg-white dark:bg-gray-900 hover:bg-gray-50 flex items-center gap-1.5"><i class="fas fa-columns"></i> Columns</button>
<div x-cloak x-show="open" @click.away="open = false" class="absolute right-0 mt-1 w-48 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg z-20 py-1">
<form method="get">
{% for col in all_members_columns %}
<label class="flex items-center gap-2 px-3 py-1.5 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800/50 cursor-pointer">
<input type="checkbox" name="members_columns" value="{{ col }}" {% if col in members_columns %}checked{% endif %} class="rounded border-gray-300 text-blue-600 focus:ring-blue-500" @change="$event.target.closest('form').submit()"> {{ col|title }}
</label>
{% endfor %}
</form>
</div>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
{% if "name" in members_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>{% endif %}
{% if "email" in members_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</th>{% endif %}
{% if "joined" in members_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Joined</th>{% endif %}
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
{% for pm in members %}
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30">
{% if "name" in members_columns %}
<td class="px-4 py-3">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-xs font-semibold text-blue-600 dark:text-blue-400 shrink-0">{{ pm.user.get_full_name|default:pm.user.username|first|upper }}</div>
<div>
<p class="font-medium text-gray-900 dark:text-white">{{ pm.user.get_full_name|default:pm.user.username }}</p>
<p class="text-xs text-gray-500">@{{ pm.user.username }}</p>
</div>
</div>
</td>
{% endif %}
{% if "email" in members_columns %}<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ pm.user.email }}</td>{% endif %}
{% if "joined" in members_columns %}<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ pm.joined_at|date:"d/m/Y"|default:"-" }}</td>{% endif %}
</tr>
{% empty %}
<tr><td colspan="3" class="px-4 py-8 text-center text-sm text-gray-400">No members found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if members_paginator.paginator.num_pages > 1 %}
<div class="px-6 py-3 border-t border-gray-100 dark:border-gray-700 flex items-center justify-between text-xs text-gray-500">
<span>{{ members.start_index }}–{{ members.end_index }} of {{ members.paginator.count }}</span>
<div class="flex items-center gap-1">
{% if members.has_previous %}<a href="?members_page={{ members.previous_page_number }}&members_search={{ members_search }}" class="px-2 py-1 border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">&lsaquo;</a>{% endif %}
<span class="px-2">{{ members.number }}/{{ members.paginator.num_pages }}</span>
{% if members.has_next %}<a href="?members_page={{ members.next_page_number }}&members_search={{ members_search }}" class="px-2 py-1 border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">&rsaquo;</a>{% endif %}
</div>
</div>
{% endif %}
</div>
<!-- EPICS TAB -->
<form id="epics-bulk-delete-form" method="post" action="{% url 'epics-bulk-delete' %}">{% csrf_token %}<template x-for="id in $store.projectDetail.epicsSelectedIds" x-key="id"><input type="hidden" name="ids" :value="id"></template></form>
<div x-show="$store.projectDetail.activeTab === 'epics'" class="rounded-xl bg-white dark:bg-gray-950 shadow-sm ring-1 ring-gray-950/5 dark:ring-white/10 overflow-hidden">
<div x-show="$store.projectDetail.epicsSelectedIds.length > 0" x-cloak class="px-6 py-2 border-b border-gray-100 dark:border-gray-700 flex items-center gap-3 bg-blue-50 dark:bg-blue-900/20">
<span class="text-xs font-medium text-blue-700 dark:text-blue-300"><span x-text="$store.projectDetail.epicsSelectedIds.length"></span> selected</span>
<button type="button" @click="confirmAction({title:'Delete Epics',message:'Delete '+$store.projectDetail.epicsSelectedIds.length+' epic(s)?',confirmText:'Delete',danger:true,onConfirm:function(){ document.getElementById('epics-bulk-delete-form').submit() }})" class="text-xs text-red-600 hover:text-red-700 font-medium"><i class="fas fa-trash mr-1"></i>Delete</button>
<button type="button" @click="$store.projectDetail.epicsSelectedIds = []" class="text-xs text-gray-500 hover:text-gray-700 ml-auto"><i class="fas fa-times mr-1"></i>Clear</button>
</div>
<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">Epics</h3>
<div class="flex items-center gap-3 flex-wrap">
<form method="get" class="flex items-center gap-2">
<div class="relative">
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs"></i>
<input type="text" name="epics_search" value="{{ epics_search }}" placeholder="Search..." class="pl-8 pr-3 py-1.5 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 w-44">
</div>
{% if epics_search %}<a href="{% url 'project-detail' project.id %}" class="text-xs text-gray-500 hover:text-gray-700">Clear</a>{% endif %}
</form>
<div x-data="{ open: false }" class="relative">
<button @click="open = !open" class="text-xs border border-gray-200 dark:border-gray-600 rounded px-2.5 py-1.5 bg-white dark:bg-gray-900 hover:bg-gray-50 flex items-center gap-1.5"><i class="fas fa-columns"></i> Columns</button>
<div x-cloak x-show="open" @click.away="open = false" class="absolute right-0 mt-1 w-48 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg z-20 py-1">
<form method="get">
{% for col in all_epics_columns %}
<label class="flex items-center gap-2 px-3 py-1.5 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800/50 cursor-pointer">
<input type="checkbox" name="epics_columns" value="{{ col }}" {% if col in epics_columns %}checked{% endif %} class="rounded border-gray-300 text-blue-600 focus:ring-blue-500" @change="$event.target.closest('form').submit()"> {{ col|title }}
</label>
{% endfor %}
</form>
</div>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-3 py-3 w-10"><input type="checkbox" @change="const cbs=document.querySelectorAll('.epic-row-cb');$store.projectDetail.epicsSelectedIds=$event.target.checked?Array.from(cbs).map(c=>c.value):[]" class="rounded border-gray-300 dark:border-gray-600 text-blue-600"></th>
{% if "name" in epics_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>{% endif %}
{% if "sort_order" in epics_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Order</th>{% endif %}
{% if "start_date" in epics_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Start</th>{% endif %}
{% if "end_date" in epics_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">End</th>{% endif %}
{% if "tickets" in epics_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tickets</th>{% endif %}
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
{% for e in epics %}
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30" :class="$store.projectDetail.epicsSelectedIds.includes('{{ e.id }}') && 'bg-blue-50 dark:bg-blue-900/10'">
<td class="px-3 py-3"><input type="checkbox" class="epic-row-cb rounded border-gray-300 dark:border-gray-600 text-blue-600" value="{{ e.id }}" x-model="$store.projectDetail.epicsSelectedIds"></td>
{% if "name" in epics_columns %}<td class="px-4 py-3 font-medium text-gray-900 dark:text-white">{{ e.name }}</td>{% endif %}
{% if "sort_order" in epics_columns %}<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ e.sort_order }}</td>{% endif %}
{% if "start_date" in epics_columns %}<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ e.start_date|date:"d/m/Y"|default:"-" }}</td>{% endif %}
{% if "end_date" in epics_columns %}<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ e.end_date|date:"d/m/Y"|default:"-" }}</td>{% endif %}
{% if "tickets" in epics_columns %}<td class="px-4 py-3"><span class="px-2 py-0.5 rounded-full text-xs bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400">{{ e.ticket_count }}</span></td>{% endif %}
</tr>
{% empty %}
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400">No epics found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if epics_paginator.paginator.num_pages > 1 %}
<div class="px-6 py-3 border-t border-gray-100 dark:border-gray-700 flex items-center justify-between text-xs text-gray-500">
<span>{{ epics.start_index }}–{{ epics.end_index }} of {{ epics.paginator.count }}</span>
<div class="flex items-center gap-1">
{% if epics.has_previous %}<a href="?epics_page={{ epics.previous_page_number }}&epics_search={{ epics_search }}" class="px-2 py-1 border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">&lsaquo;</a>{% endif %}
<span class="px-2">{{ epics.number }}/{{ epics.paginator.num_pages }}</span>
{% if epics.has_next %}<a href="?epics_page={{ epics.next_page_number }}&epics_search={{ epics_search }}" class="px-2 py-1 border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">&rsaquo;</a>{% endif %}
</div>
</div>
{% endif %}
</div>
<!-- TICKETS TAB -->
<form id="tickets-bulk-delete-form" method="post" action="{% url 'tickets-bulk-delete' %}">{% csrf_token %}<template x-for="id in $store.projectDetail.ticketsSelectedIds" x-key="id"><input type="hidden" name="ids" :value="id"></template></form>
<div x-show="$store.projectDetail.activeTab === 'tickets'" class="rounded-xl bg-white dark:bg-gray-950 shadow-sm ring-1 ring-gray-950/5 dark:ring-white/10 overflow-hidden">
<div x-show="$store.projectDetail.ticketsSelectedIds.length > 0" x-cloak class="px-6 py-2 border-b border-gray-100 dark:border-gray-700 flex items-center gap-3 bg-blue-50 dark:bg-blue-900/20">
<span class="text-xs font-medium text-blue-700 dark:text-blue-300"><span x-text="$store.projectDetail.ticketsSelectedIds.length"></span> selected</span>
<button type="button" @click="confirmAction({title:'Delete Tickets',message:'Delete '+$store.projectDetail.ticketsSelectedIds.length+' ticket(s)?',confirmText:'Delete',danger:true,onConfirm:function(){ document.getElementById('tickets-bulk-delete-form').submit() }})" class="text-xs text-red-600 hover:text-red-700 font-medium"><i class="fas fa-trash mr-1"></i>Delete</button>
<button type="button" @click="$store.projectDetail.ticketsSelectedIds = []" class="text-xs text-gray-500 hover:text-gray-700 ml-auto"><i class="fas fa-times mr-1"></i>Clear</button>
</div>
<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">Tickets</h3>
<div class="flex items-center gap-3 flex-wrap">
<form method="get" class="flex items-center gap-2">
<div class="relative">
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs"></i>
<input type="text" name="tickets_search" value="{{ tickets_search }}" placeholder="Search..." class="pl-8 pr-3 py-1.5 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 w-44">
</div>
{% if tickets_search or tickets_status or tickets_priority %}<a href="{% url 'project-detail' project.id %}" class="text-xs text-gray-500 hover:text-gray-700">Clear</a>{% endif %}
</form>
<select name="tickets_status" onchange="window.location.href='?tickets_status='+this.value" class="text-xs border border-gray-200 dark:border-gray-600 rounded px-2 py-1.5 bg-white dark:bg-gray-900">
<option value="">All Statuses</option>
{% for s in statuses %}
<option value="{{ s.id }}" {% if tickets_status == s.id|stringformat:"s" %}selected{% endif %}>{{ s.name }}</option>
{% endfor %}
</select>
<select name="tickets_priority" onchange="window.location.href='?tickets_priority='+this.value" class="text-xs border border-gray-200 dark:border-gray-600 rounded px-2 py-1.5 bg-white dark:bg-gray-900">
<option value="">All Priorities</option>
{% for p in priorities %}
<option value="{{ p.id }}" {% if tickets_priority == p.id|stringformat:"s" %}selected{% endif %}>{{ p.name }}</option>
{% endfor %}
</select>
<div x-data="{ open: false }" class="relative">
<button @click="open = !open" class="text-xs border border-gray-200 dark:border-gray-600 rounded px-2.5 py-1.5 bg-white dark:bg-gray-900 hover:bg-gray-50 flex items-center gap-1.5"><i class="fas fa-columns"></i> Columns</button>
<div x-cloak x-show="open" @click.away="open = false" class="absolute right-0 mt-1 w-48 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg z-20 py-1">
<form method="get">
{% for col in all_ticket_columns %}
<label class="flex items-center gap-2 px-3 py-1.5 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800/50 cursor-pointer">
<input type="checkbox" name="tickets_columns" value="{{ col }}" {% if col in tickets_columns %}checked{% endif %} class="rounded border-gray-300 text-blue-600 focus:ring-blue-500" @change="$event.target.closest('form').submit()"> {{ col|title }}
</label>
{% endfor %}
</form>
</div>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-3 py-3 w-10"><input type="checkbox" @change="const cbs=document.querySelectorAll('.ticket-row-cb');$store.projectDetail.ticketsSelectedIds=$event.target.checked?Array.from(cbs).map(c=>c.value):[]" class="rounded border-gray-300 dark:border-gray-600 text-blue-600"></th>
{% if "uuid" in tickets_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>{% endif %}
{% if "name" in tickets_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>{% endif %}
{% if "status" in tickets_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>{% endif %}
{% if "priority" in tickets_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Priority</th>{% endif %}
{% if "assignees" in tickets_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Assignees</th>{% endif %}
{% if "start_date" in tickets_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Start</th>{% endif %}
{% if "due_date" in tickets_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Due</th>{% endif %}
{% if "created_at" in tickets_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>{% endif %}
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
{% for t in tickets %}
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30" :class="$store.projectDetail.ticketsSelectedIds.includes('{{ t.id }}') && 'bg-blue-50 dark:bg-blue-900/10'">
<td class="px-3 py-3"><input type="checkbox" class="ticket-row-cb rounded border-gray-300 dark:border-gray-600 text-blue-600" value="{{ t.id }}" x-model="$store.projectDetail.ticketsSelectedIds"></td>
{% if "uuid" in tickets_columns %}<td class="px-4 py-3 text-xs font-mono text-gray-500 dark:text-gray-400">{{ t.uuid }}</td>{% endif %}
{% if "name" in tickets_columns %}<td class="px-4 py-3 font-medium text-gray-900 dark:text-white max-w-xs truncate">{{ t.name }}</td>{% endif %}
{% if "status" in tickets_columns %}<td class="px-4 py-3"><span class="px-2 py-0.5 rounded text-xs font-medium" style="color:{{ t.status.color }};background-color:{{ t.status.color }}20">{{ t.status.name }}</span></td>{% endif %}
{% if "priority" in tickets_columns %}<td class="px-4 py-3">{% if t.priority %}<span class="px-2 py-0.5 rounded text-xs font-medium" style="color:{{ t.priority.color }};background-color:{{ t.priority.color }}20">{{ t.priority.name }}</span>{% else %}<span class="text-gray-400">-</span>{% endif %}</td>{% endif %}
{% if "assignees" in tickets_columns %}<td class="px-4 py-3">{% if t.assignees.all %}<div class="flex -space-x-1.5">{% for a in t.assignees.all|slice:":3" %}<div class="w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-[10px] font-semibold text-blue-600 border-2 border-white dark:border-gray-900" title="{{ a.get_full_name|default:a.username }}">{{ a.get_full_name|default:a.username|first|upper }}</div>{% endfor %}</div>{% else %}<span class="text-gray-400">-</span>{% endif %}</td>{% endif %}
{% if "start_date" in tickets_columns %}<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ t.start_date|date:"d/m/Y"|default:"-" }}</td>{% endif %}
{% if "due_date" in tickets_columns %}<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ t.due_date|date:"d/m/Y"|default:"-" }}</td>{% endif %}
{% if "created_at" in tickets_columns %}<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ t.created_at|date:"d/m/Y" }}</td>{% endif %}
</tr>
{% empty %}
<tr><td colspan="10" class="px-4 py-8 text-center text-sm text-gray-400">No tickets found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if tickets_paginator.paginator.num_pages > 1 %}
<div class="px-6 py-3 border-t border-gray-100 dark:border-gray-700 flex items-center justify-between text-xs text-gray-500">
<span>{{ tickets.start_index }}–{{ tickets.end_index }} of {{ tickets.paginator.count }}</span>
<div class="flex items-center gap-1">
{% if tickets.has_previous %}<a href="?tickets_page={{ tickets.previous_page_number }}&tickets_search={{ tickets_search }}&tickets_status={{ tickets_status }}&tickets_priority={{ tickets_priority }}" class="px-2 py-1 border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">&lsaquo;</a>{% endif %}
<span class="px-2">{{ tickets.number }}/{{ tickets.paginator.num_pages }}</span>
{% if tickets.has_next %}<a href="?tickets_page={{ tickets.next_page_number }}&tickets_search={{ tickets_search }}&tickets_status={{ tickets_status }}&tickets_priority={{ tickets_priority }}" class="px-2 py-1 border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">&rsaquo;</a>{% endif %}
</div>
</div>
{% endif %}
</div>
<!-- NOTES TAB -->
<form id="notes-bulk-delete-form" method="post" action="{% url 'notes-bulk-delete' %}">{% csrf_token %}<template x-for="id in $store.projectDetail.notesSelectedIds" x-key="id"><input type="hidden" name="ids" :value="id"></template></form>
<div x-show="$store.projectDetail.activeTab === 'notes'" class="rounded-xl bg-white dark:bg-gray-950 shadow-sm ring-1 ring-gray-950/5 dark:ring-white/10 overflow-hidden">
<div x-show="$store.projectDetail.notesSelectedIds.length > 0" x-cloak class="px-6 py-2 border-b border-gray-100 dark:border-gray-700 flex items-center gap-3 bg-blue-50 dark:bg-blue-900/20">
<span class="text-xs font-medium text-blue-700 dark:text-blue-300"><span x-text="$store.projectDetail.notesSelectedIds.length"></span> selected</span>
<button type="button" @click="confirmAction({title:'Delete Notes',message:'Delete '+$store.projectDetail.notesSelectedIds.length+' note(s)?',confirmText:'Delete',danger:true,onConfirm:function(){ document.getElementById('notes-bulk-delete-form').submit() }})" class="text-xs text-red-600 hover:text-red-700 font-medium"><i class="fas fa-trash mr-1"></i>Delete</button>
<button type="button" @click="$store.projectDetail.notesSelectedIds = []" class="text-xs text-gray-500 hover:text-gray-700 ml-auto"><i class="fas fa-times mr-1"></i>Clear</button>
</div>
<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">Project Notes</h3>
<div class="flex items-center gap-3 flex-wrap">
<form method="get" class="flex items-center gap-2">
<div class="relative">
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs"></i>
<input type="text" name="notes_search" value="{{ notes_search }}" placeholder="Search..." class="pl-8 pr-3 py-1.5 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 w-44">
</div>
{% if notes_search %}<a href="{% url 'project-detail' project.id %}" class="text-xs text-gray-500 hover:text-gray-700">Clear</a>{% endif %}
</form>
<div x-data="{ open: false }" class="relative">
<button @click="open = !open" class="text-xs border border-gray-200 dark:border-gray-600 rounded px-2.5 py-1.5 bg-white dark:bg-gray-900 hover:bg-gray-50 flex items-center gap-1.5"><i class="fas fa-columns"></i> Columns</button>
<div x-cloak x-show="open" @click.away="open = false" class="absolute right-0 mt-1 w-48 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg z-20 py-1">
<form method="get">
{% for col in all_notes_columns %}
<label class="flex items-center gap-2 px-3 py-1.5 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800/50 cursor-pointer">
<input type="checkbox" name="notes_columns" value="{{ col }}" {% if col in notes_columns %}checked{% endif %} class="rounded border-gray-300 text-blue-600 focus:ring-blue-500" @change="$event.target.closest('form').submit()"> {{ col|title }}
</label>
{% endfor %}
</form>
</div>
</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-3 py-3 w-10"><input type="checkbox" @change="const cbs=document.querySelectorAll('.note-row-cb');$store.projectDetail.notesSelectedIds=$event.target.checked?Array.from(cbs).map(c=>c.value):[]" class="rounded border-gray-300 dark:border-gray-600 text-blue-600"></th>
{% if "title" in notes_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Title</th>{% endif %}
{% if "date" in notes_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>{% endif %}
{% if "content" in notes_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Content</th>{% endif %}
{% if "created_by" in notes_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created By</th>{% endif %}
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
{% for n in notes %}
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30" :class="$store.projectDetail.notesSelectedIds.includes('{{ n.id }}') && 'bg-blue-50 dark:bg-blue-900/10'">
<td class="px-3 py-3"><input type="checkbox" class="note-row-cb rounded border-gray-300 dark:border-gray-600 text-blue-600" value="{{ n.id }}" x-model="$store.projectDetail.notesSelectedIds"></td>
{% if "title" in notes_columns %}<td class="px-4 py-3 font-medium text-gray-900 dark:text-white">{{ n.title }}</td>{% endif %}
{% if "date" in notes_columns %}<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ n.note_date|date:"d/m/Y" }}</td>{% endif %}
{% if "content" in notes_columns %}<td class="px-4 py-3 text-gray-500 dark:text-gray-400 max-w-xs truncate">{{ n.content|striptags|truncatewords:20|default:"-" }}</td>{% endif %}
{% if "created_by" in notes_columns %}<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ n.created_by.get_full_name|default:n.created_by.username }}</td>{% endif %}
<td class="px-4 py-3">
<button type="button" @click="$store.projectDetail.viewNoteData = {id: '{{ n.id }}', title: '{{ n.title|escapejs }}', content: '{{ n.content|escapejs }}'}; $store.projectDetail.viewNoteOpen = true" class="inline-flex items-center px-2 py-1 text-xs font-medium rounded border border-blue-200 dark:border-blue-800 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20">View</button>
</td>
</tr>
{% empty %}
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400">No notes found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if notes_paginator.paginator.num_pages > 1 %}
<div class="px-6 py-3 border-t border-gray-100 dark:border-gray-700 flex items-center justify-between text-xs text-gray-500">
<span>{{ notes.start_index }}–{{ notes.end_index }} of {{ notes.paginator.count }}</span>
<div class="flex items-center gap-1">
{% if notes.has_previous %}<a href="?notes_page={{ notes.previous_page_number }}&notes_search={{ notes_search }}" class="px-2 py-1 border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">&lsaquo;</a>{% endif %}
<span class="px-2">{{ notes.number }}/{{ notes.paginator.num_pages }}</span>
{% if notes.has_next %}<a href="?notes_page={{ notes.next_page_number }}&notes_search={{ notes_search }}" class="px-2 py-1 border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">&rsaquo;</a>{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
<!-- View Note Modal -->
<div x-cloak x-show="$store.projectDetail.viewNoteOpen" class="fixed inset-0 z-50 flex items-center justify-center">
<div class="fixed inset-0 bg-black/50" @click="$store.projectDetail.viewNoteOpen = 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="px-8 py-5 flex items-center justify-between border-b border-gray-100 dark:border-gray-700">
<h3 class="text-xl font-bold text-gray-900 dark:text-white" x-text="$store.projectDetail.viewNoteData.title">Note</h3>
<button @click="$store.projectDetail.viewNoteOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<i class="fas fa-times text-lg"></i>
</button>
</div>
<div class="px-8 py-6 prose prose-sm dark:prose-invert max-w-none" x-html="$store.projectDetail.viewNoteData.content"></div>
<div class="px-8 py-5 flex justify-end gap-3 border-t border-gray-100 dark:border-gray-700">
<button type="button" @click="$store.projectDetail.viewNoteOpen = false" class="px-4 py-2 text-sm 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">Close</button>
</div>
</div>
</div>
{% 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>
<!-- Board Permissions Modal -->
<div x-cloak x-show="$store.projectDetail.permissionsOpen" class="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
@keydown.escape.window="$store.projectDetail.permissionsOpen = false">
<div class="fixed inset-0 bg-black/50" @click="$store.projectDetail.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="$store.projectDetail.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' 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 in role_choices %}
{% if role.slug not in admin_roles %}
<option value="{{ role.slug }}">{{ role.name }}</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 s in project.ticket_statuses.all %}
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="move_to_statuses" value="{{ s.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:{{ s.color }}"></span>
<span class="text-xs text-gray-700 dark:text-gray-300 truncate">{{ s.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.role_display_name }}</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' project.id perm.id %}" class="inline">
{% csrf_token %}
<button type="button" @click="confirmAction({title:'Delete Permission', message:'Delete permission for <strong>' + '{{ perm.role_display_name|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>
{% include "projects/partials/board_automation_modal.html" with selected_project=project %}
{% endblock %}
{% block extra_scripts %}
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('projectDetail', {
activeTab: 'statuses',
externalModalOpen: false,
epicsSelectedIds: [],
ticketsSelectedIds: [],
notesSelectedIds: [],
viewNoteOpen: false,
viewNoteData: { id: '', title: '', content: '' },
manageStatusesOpen: false,
permissionsOpen: false,
automationOpen: false,
createStatusName: '',
createStatusColor: '#6B7280',
createStatusOrder: '0',
createStatusCompleted: false,
editStatusId: null,
editStatusName: '',
editStatusColor: '',
editStatusOrder: '0',
editStatusCompleted: false,
});
});
</script>
{% endblock %}