Enhances user experience with dark‑mode detection, responsive layout, and streamlined role selection workflow
1061 lines
86 KiB
HTML
1061 lines
86 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}{% if project %}Edit {{ project.name }}{% else %}Create Project{% endif %}{% endblock %}
|
||
{% block extra_head %}
|
||
{% include "components/rich_text_editor_assets.html" %}
|
||
{% endblock %}
|
||
{% block breadcrumb %}
|
||
<nav class="mb-4" aria-label="Breadcrumb">
|
||
<ol class="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
|
||
<li><a href="{% url 'projects-list' %}" class="hover:text-gray-700 dark:hover:text-gray-300">Projects</a></li>
|
||
<li class="text-gray-300 dark:text-gray-600"><i class="fas fa-chevron-right text-[10px]"></i></li>
|
||
{% if project %}
|
||
<li><a href="{% url 'project-detail' project.id %}" class="hover:text-gray-700 dark:hover:text-gray-300">{{ project.name }}</a></li>
|
||
<li class="text-gray-300 dark:text-gray-600"><i class="fas fa-chevron-right text-[10px]"></i></li>
|
||
<li class="text-gray-900 dark:text-white font-medium" aria-current="page">Edit</li>
|
||
{% else %}
|
||
<li class="text-gray-900 dark:text-white font-medium" aria-current="page">Create</li>
|
||
{% endif %}
|
||
</ol>
|
||
</nav>
|
||
{% endblock %}
|
||
{% block content %}
|
||
<div x-data="{
|
||
externalModalOpen: false, activeTab: new URLSearchParams(window.location.search).has('add_member') ? 'members' : 'details',
|
||
editStatusOpen: false,
|
||
editStatusData: { id: '', name: '', color: '', sort_order: '', is_completed: false },
|
||
createStatusOpen: false,
|
||
createStatusData: { name: '', color: '#6B7280', sort_order: '0', is_completed: false },
|
||
createMemberOpen: new URLSearchParams(window.location.search).has('add_member'),
|
||
createMemberData: { user_id: '' },
|
||
createUserOpen: false,
|
||
createNoteOpen: false,
|
||
createNoteData: { title: '', content: '' },
|
||
editNoteOpen: false,
|
||
editNoteData: { id: '', title: '', content: '' },
|
||
editEpicOpen: false,
|
||
editEpicData: { id: '', name: '', description: '', sort_order: '', start_date: '', end_date: '' },
|
||
createEpicOpen: false,
|
||
createEpicData: { name: '', description: '', sort_order: '0', start_date: '', end_date: '' },
|
||
editTicketOpen: false,
|
||
editTicketData: { id: '', name: '', description: '', status_id: '', priority_id: '', start_date: '', due_date: '' },
|
||
createTicketOpen: false,
|
||
createTicketData: { name: '', description: '', status_id: '', priority_id: '', start_date: '', due_date: '' },
|
||
selectedMemberIds: [], selectAllMembers: false,
|
||
selectedEpicIds: [], selectAllEpics: false,
|
||
selectedTicketIds: [], selectAllTickets: false,
|
||
selectedNoteIds: [], selectAllNotes: false,
|
||
selectedStatusIds: [], selectAllStatuses: false
|
||
}">
|
||
<div class="w-full">
|
||
<div class="mb-6 flex items-center justify-between">
|
||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white tracking-tight">{% if project %}Edit Project{% else %}Create Project{% endif %}</h1>
|
||
<div class="flex items-center gap-2">
|
||
{% if project %}
|
||
<button type="button" @click="externalModalOpen = true" class="inline-flex items-center gap-1.5 px-3 py-1.5 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"><i class="fas fa-globe"></i> External Dashboard</button>
|
||
<form method="post" action="{% url 'project-delete' project.id %}" class="inline">
|
||
{% csrf_token %}
|
||
<button type="button" @click="confirmAction({title:'Delete Project', message:'Delete this project and all its tickets?', confirmText:'Delete', danger:true, onConfirm:function(){ $el.closest('form').submit() }})" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm 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> Delete</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<form method="post" action="{% if project %}{% url 'project-edit' project.id %}{% else %}{% url 'project-create' %}{% endif %}" >
|
||
{% csrf_token %}
|
||
|
||
<div class="space-y-6">
|
||
<!-- Full width section -->
|
||
<div class="space-y-6">
|
||
<div>
|
||
<label for="id_name" class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">Name <span class="text-red-500">*</span></label>
|
||
<input type="text" id="id_name" name="name" value="{{ project.name|default:'' }}" 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 transition-colors">
|
||
</div>
|
||
|
||
<div>
|
||
<label for="id_description" 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" initial_value=project.description %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 2 Column Section -->
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<div>
|
||
<label for="id_ticket_prefix" class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">Ticket prefix <span class="text-red-500">*</span></label>
|
||
<input type="text" id="id_ticket_prefix" name="ticket_prefix" value="{{ project.ticket_prefix|default:'' }}" maxlength="10"
|
||
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 for="id_color" class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">Project Color</label>
|
||
<div class="flex items-center gap-2" x-data="{ color: '{{ project.color|default:'#3b82f6' }}' }">
|
||
<toolcool-color-picker popup-position="left" :color="color" @change="color = $event.detail.hex" class="shrink-0"></toolcool-color-picker>
|
||
<input type="text" id="id_color" name="color" x-model="color"
|
||
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>
|
||
<p class="text-xs text-gray-500 mt-1.5">Choose a color for the project card and badge</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label for="id_start_date" class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">Start Date</label>
|
||
<input type="date" id="id_start_date" name="start_date" value="{{ project.start_date|date:'Y-m-d'|default:'' }}"
|
||
onclick="this.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 for="id_end_date" class="block text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1.5">End Date</label>
|
||
<input type="date" id="id_end_date" name="end_date" value="{{ project.end_date|date:'Y-m-d'|default:'' }}"
|
||
onclick="this.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>
|
||
|
||
{% if not project %}
|
||
<div x-data="{ useDefaultStatuses: true }">
|
||
<input type="hidden" name="use_default_statuses" :value="useDefaultStatuses ? '1' : '0'">
|
||
<label class="flex items-start gap-3 cursor-pointer" @click.prevent="useDefaultStatuses = !useDefaultStatuses">
|
||
<div role="switch" class="mt-0.5 relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none" :style="useDefaultStatuses ? 'background-color: var(--color-primary, #3b82f6)' : 'background-color: #d1d5db'">
|
||
<span class="pointer-events-none relative inline-block h-4 w-4 rounded-full bg-white shadow-sm ring-0 transition-transform duration-200 ease-in-out" :style="useDefaultStatuses ? 'transform: translateX(16px)' : 'transform: translateX(0)'"></span>
|
||
</div>
|
||
<div>
|
||
<p class="text-sm font-semibold text-gray-900 dark:text-white">Use Default Ticket Statuses</p>
|
||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">Create standard Backlog, To Do, In Progress, Review, and Done statuses automatically</p>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
{% else %}
|
||
<div></div>
|
||
{% endif %}
|
||
|
||
<div x-data="{ isPinned: {{ project.pinned_date|yesno:'true,false,false' }} }">
|
||
<input type="hidden" name="is_pinned" :value="isPinned ? '1' : '0'">
|
||
<label class="flex items-start gap-3 cursor-pointer" @click.prevent="isPinned = !isPinned">
|
||
<div role="switch" class="mt-0.5 relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none" :style="isPinned ? 'background-color: var(--color-primary, #3b82f6)' : 'background-color: #d1d5db'">
|
||
<span class="pointer-events-none relative inline-block h-4 w-4 rounded-full bg-white shadow-sm ring-0 transition-transform duration-200 ease-in-out" :style="isPinned ? 'transform: translateX(16px)' : 'transform: translateX(0)'"></span>
|
||
</div>
|
||
<div>
|
||
<p class="text-sm font-semibold text-gray-900 dark:text-white">Pin Project</p>
|
||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">Pinned projects will appear in the dashboard timeline</p>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-3 mt-8">
|
||
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white font-medium text-sm px-4 py-2 rounded-lg transition-colors">
|
||
{% if project %}Save Changes{% else %}Create{% endif %}
|
||
</button>
|
||
{% if not project %}
|
||
<button type="submit" name="create_another" value="1" class="bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-900 dark:text-white font-medium text-sm px-4 py-2 rounded-lg transition-colors">
|
||
Create & create another
|
||
</button>
|
||
{% endif %}
|
||
<a href="{% url 'projects-list' %}" class="bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-900 dark:text-white font-medium text-sm px-4 py-2 rounded-lg transition-colors">Cancel</a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
{% if project %}
|
||
<!-- ── Relation Sections (edit page) ──────────────────────────────────── -->
|
||
<div class="max-w-full space-y-6 mt-8" x-data="{ activeTab: 'statuses' }">
|
||
{% include "components/project_tabs.html" with active_tab_var="activeTab" %}
|
||
|
||
{% if statuses %}
|
||
<div x-show="activeTab === 'statuses'" x-cloak 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-600 flex flex-wrap items-center justify-between gap-4">
|
||
<h3 class="text-base font-semibold text-gray-900 dark:text-white">Ticket Statuses</h3>
|
||
<button type="button" @click="createStatusData = { name: '', color: '#6B7280', sort_order: '0', is_completed: false }; createStatusOpen = true" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors shadow-sm"><i class="fas fa-plus"></i> New Status</button>
|
||
</div>
|
||
<form id="statuses-bulk-delete-form" method="post" action="{% url 'ticket-statuses-bulk-delete' %}">
|
||
{% csrf_token %}
|
||
<input type="hidden" name="project_id" value="{{ project.id }}">
|
||
<template x-for="id in selectedStatusIds" x-key="id">
|
||
<input type="hidden" name="ids" :value="id">
|
||
</template>
|
||
</form>
|
||
<div x-show="selectedStatusIds.length > 0" x-cloak class="mb-4 flex items-center gap-3 px-4 py-2.5 bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-lg">
|
||
<span class="text-sm font-medium text-primary-700 dark:text-primary-300">
|
||
<span x-text="selectedStatusIds.length"></span> selected
|
||
</span>
|
||
<button type="button" @click="confirmAction({title:'Delete Statuses', message:'Delete ' + selectedStatusIds.length + ' status(es)? This cannot be undone.', confirmText:'Delete', danger:true, onConfirm:function(){ document.getElementById('statuses-bulk-delete-form').submit() }})" class="dk-btn dk-btn-danger text-xs px-3 py-1">
|
||
<i class="fas fa-trash mr-1"></i> Delete Selected
|
||
</button>
|
||
<button type="button" @click="selectedStatusIds = []; selectAllStatuses = false" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 ml-auto">
|
||
<i class="fas fa-times mr-1"></i> Clear selection
|
||
</button>
|
||
</div>
|
||
<div class="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 w-10">
|
||
<input type="checkbox" @change="selectAllStatuses = !selectAllStatuses; selectedStatusIds = selectAllStatuses ? Array.from(document.querySelectorAll('.status-checkbox')).map(cb => cb.value) : []" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
||
</th>
|
||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status 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 Status</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-600">
|
||
{% for s in statuses %}
|
||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30" :class="selectedStatusIds.includes('{{ s.id }}') && 'bg-blue-50 dark:bg-blue-900/10'">
|
||
<td class="px-4 py-3">
|
||
<input type="checkbox" class="status-checkbox rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" :value="'{{ s.id }}'" x-model="selectedStatusIds">
|
||
</td>
|
||
<td class="px-4 py-3 text-gray-900 dark:text-white font-medium">{{ 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">
|
||
<button type="button" @click="editStatusData = {id: '{{ s.id }}', name: '{{ s.name|escapejs }}', color: '{{ s.color|escapejs }}', sort_order: '{{ s.sort_order }}', is_completed: {{ s.is_completed|lower }}}; editStatusOpen = true" 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-2">Edit</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:'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">Delete</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400">No statuses found.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-show="activeTab === 'members'" x-cloak 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-600 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-2">
|
||
<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 members..." 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-950 focus:outline-none focus:ring-2 focus:ring-blue-500 w-44">
|
||
</div>
|
||
{% if members_search %}<a href="{% url 'project-edit' project.id %}" class="text-xs text-gray-500 hover:text-gray-700">Clear</a>{% endif %}
|
||
</form>
|
||
<button type="button" @click="createMemberData = { user_id: '' }; createMemberOpen = true" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors shadow-sm"><i class="fas fa-plus"></i> Add Member</button>
|
||
</div>
|
||
</div>
|
||
<form id="members-bulk-delete-form" method="post" action="{% url 'project-member-bulk-delete' %}">
|
||
{% csrf_token %}
|
||
<input type="hidden" name="user_id" value="{{ project.id }}">
|
||
<template x-for="id in selectedMemberIds" x-key="id">
|
||
<input type="hidden" name="member_ids" :value="id">
|
||
</template>
|
||
</form>
|
||
<div x-show="selectedMemberIds.length > 0" x-cloak class="mb-4 flex items-center gap-3 px-4 py-2.5 bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-lg">
|
||
<span class="text-sm font-medium text-primary-700 dark:text-primary-300">
|
||
<span x-text="selectedMemberIds.length"></span> selected
|
||
</span>
|
||
<button type="button" @click="confirmAction({title:'Remove Members', message:'Remove ' + selectedMemberIds.length + ' member(s)?', confirmText:'Remove', danger:true, onConfirm:function(){ document.getElementById('members-bulk-delete-form').submit() }})" class="dk-btn dk-btn-danger text-xs px-3 py-1">
|
||
<i class="fas fa-trash mr-1"></i> Remove Selected
|
||
</button>
|
||
<button type="button" @click="selectedMemberIds = []; selectAllMembers = false" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 ml-auto">
|
||
<i class="fas fa-times mr-1"></i> Clear selection
|
||
</button>
|
||
</div>
|
||
<div class="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 w-10">
|
||
<input type="checkbox" @change="selectAllMembers = !selectAllMembers; selectedMemberIds = selectAllMembers ? Array.from(document.querySelectorAll('.member-checkbox')).map(cb => cb.value) : []" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
||
</th>
|
||
<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">Email</th>
|
||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Joined</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-600">
|
||
{% for pm in members %}
|
||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30" :class="selectedMemberIds.includes('{{ pm.id }}') && 'bg-blue-50 dark:bg-blue-900/10'">
|
||
<td class="px-4 py-3">
|
||
<input type="checkbox" class="member-checkbox rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" :value="'{{ pm.id }}'" x-model="selectedMemberIds">
|
||
</td>
|
||
<td class="px-4 py-3">
|
||
<div class="flex items-center gap-2.5">
|
||
<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 flex-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>
|
||
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ pm.user.email|default:"-" }}</td>
|
||
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ pm.created_at|date:"d/m/Y" }}</td></td>
|
||
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||
<form method="post" action="{% url 'project-member-delete' pm.id %}" class="inline">
|
||
{% csrf_token %}
|
||
<button type="button" @click="confirmAction({title:'Remove Member', message:'Remove this member from the project?', confirmText:'Remove', 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">Delete</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr><td colspan="5" class="px-4 py-8 text-center text-sm text-gray-400">No members found.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
{% if epics %}
|
||
<div x-show="activeTab === 'epics'" x-cloak 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-600 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-2">
|
||
<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 epics..." 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-950 focus:outline-none focus:ring-2 focus:ring-blue-500 w-44">
|
||
</div>
|
||
<input type="hidden" name="epics_per_page" value="{{ epics_per_page }}">
|
||
{% if epics_search %}<a href="{% url 'project-edit' project.id %}" class="text-xs text-gray-500 hover:text-gray-700">Clear</a>{% endif %}
|
||
</form>
|
||
<button type="button" @click="createEpicData = { name: '', description: '', sort_order: '0', start_date: '', end_date: '' }; createEpicOpen = true" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors shadow-sm"><i class="fas fa-plus"></i> New Epic</button>
|
||
</div>
|
||
</div>
|
||
<form id="epics-bulk-delete-form" method="post" action="{% url 'epics-bulk-delete' %}">
|
||
{% csrf_token %}
|
||
<template x-for="id in selectedEpicIds" x-key="id">
|
||
<input type="hidden" name="ids" :value="id">
|
||
</template>
|
||
</form>
|
||
<div x-show="selectedEpicIds.length > 0" x-cloak class="mb-4 flex items-center gap-3 px-4 py-2.5 bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-lg">
|
||
<span class="text-sm font-medium text-primary-700 dark:text-primary-300">
|
||
<span x-text="selectedEpicIds.length"></span> selected
|
||
</span>
|
||
<button type="button" @click="confirmAction({title:'Delete Epics', message:'Delete ' + selectedEpicIds.length + ' epic(s)?', confirmText:'Delete', danger:true, onConfirm:function(){ document.getElementById('epics-bulk-delete-form').submit() }})" class="dk-btn dk-btn-danger text-xs px-3 py-1">
|
||
<i class="fas fa-trash mr-1"></i> Delete Selected
|
||
</button>
|
||
<button type="button" @click="selectedEpicIds = []; selectAllEpics = false" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 ml-auto">
|
||
<i class="fas fa-times mr-1"></i> Clear selection
|
||
</button>
|
||
</div>
|
||
<div class="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 w-10">
|
||
<input type="checkbox" @change="selectAllEpics = !selectAllEpics; selectedEpicIds = selectAllEpics ? Array.from(document.querySelectorAll('.epic-checkbox')).map(cb => cb.value) : []" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
||
</th>
|
||
<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">Sort Order</th>
|
||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Start Date</th>
|
||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">End Date</th>
|
||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Tickets</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-600">
|
||
{% for e in epics %}
|
||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30" :class="selectedEpicIds.includes('{{ e.id }}') && 'bg-blue-50 dark:bg-blue-900/10'">
|
||
<td class="px-4 py-3">
|
||
<input type="checkbox" class="epic-checkbox rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" :value="'{{ e.id }}'" x-model="selectedEpicIds">
|
||
</td>
|
||
<td class="px-4 py-3 font-medium text-gray-900 dark:text-white">{{ e.name }}</td>
|
||
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ e.sort_order }}</td>
|
||
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ e.start_date|date:"d/m/Y"|default:"-" }}</td>
|
||
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ e.end_date|date:"d/m/Y"|default:"-" }}</td>
|
||
<td class="px-4 py-3"><span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400">{{ e.ticket_count }}</span></td>
|
||
<td class="px-4 py-3">
|
||
<div class="flex items-center gap-1.5">
|
||
<button type="button" @click="editEpicData = {id: '{{ e.id }}', name: '{{ e.name|escapejs }}', description: '{{ e.description|escapejs }}', sort_order: '{{ e.sort_order }}', start_date: '{{ e.start_date|date:'Y-m-d' }}', end_date: '{{ e.end_date|date:'Y-m-d' }}'}; editEpicOpen = true" class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors shadow-sm">Edit</button>
|
||
<form method="post" action="{% url 'epic-delete' e.id %}" class="inline">{% csrf_token %}<button type="button" @click="confirmAction({title:'Delete Epic', message:'Delete this epic?', 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">Delete</button></form>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr><td colspan="7" 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-600 flex items-center justify-between">
|
||
<p class="text-xs text-gray-500">Showing {{ epics.start_index }}–{{ epics.end_index }} of {{ epics.paginator.count }}</p>
|
||
<div class="flex items-center gap-1">
|
||
<form method="get" style="display:inline">
|
||
{% if epics_search %}<input type="hidden" name="epics_search" value="{{ epics_search }}">{% endif %}
|
||
<select name="epics_per_page" onchange="this.form.submit()" class="text-xs border border-gray-200 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-950">
|
||
<option value="10" {% if epics_per_page == 10 %}selected{% endif %}>10</option>
|
||
<option value="20" {% if epics_per_page == 20 %}selected{% endif %}>20</option>
|
||
<option value="50" {% if epics_per_page == 50 %}selected{% endif %}>50</option>
|
||
</select>
|
||
</form>
|
||
<div class="flex items-center gap-1 ml-2">
|
||
{% if epics.has_previous %}
|
||
<a href="?epics_page=1{% if epics_search %}&epics_search={{ epics_search }}{% endif %}&epics_per_page={{ epics_per_page }}" class="px-2 py-1 text-xs border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">«</a>
|
||
<a href="?epics_page={{ epics.previous_page_number }}{% if epics_search %}&epics_search={{ epics_search }}{% endif %}&epics_per_page={{ epics_per_page }}" class="px-2 py-1 text-xs border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">‹</a>
|
||
{% endif %}
|
||
<span class="px-2 py-1 text-xs text-gray-500">Page {{ epics.number }}/{{ epics.paginator.num_pages }}</span>
|
||
{% if epics.has_next %}
|
||
<a href="?epics_page={{ epics.next_page_number }}{% if epics_search %}&epics_search={{ epics_search }}{% endif %}&epics_per_page={{ epics_per_page }}" class="px-2 py-1 text-xs border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">›</a>
|
||
<a href="?epics_page={{ epics.paginator.num_pages }}{% if epics_search %}&epics_search={{ epics_search }}{% endif %}&epics_per_page={{ epics_per_page }}" class="px-2 py-1 text-xs border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">»</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if full_tickets %}
|
||
<div x-show="activeTab === 'tickets'" x-cloak 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-600 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-2 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 tickets..." 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-950 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-edit' project.id %}" class="text-xs text-gray-500 hover:text-gray-700">Clear</a>{% endif %}
|
||
</form>
|
||
<form method="get" class="flex items-center gap-2">
|
||
<select name="tickets_status" onchange="this.form.submit()" class="text-xs border border-gray-200 dark:border-gray-600 rounded px-2 py-1.5 bg-white dark:bg-gray-950">
|
||
<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>
|
||
<input type="hidden" name="tickets_per_page" value="{{ tickets_per_page }}">
|
||
</form>
|
||
<form method="get" class="flex items-center gap-2">
|
||
<select name="tickets_priority" onchange="this.form.submit()" class="text-xs border border-gray-200 dark:border-gray-600 rounded px-2 py-1.5 bg-white dark:bg-gray-950">
|
||
<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>
|
||
<input type="hidden" name="tickets_per_page" value="{{ tickets_per_page }}">
|
||
</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-950 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-950 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>
|
||
<a href="{% url 'tickets-import' %}?project={{ project.id }}" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors shadow-sm"><i class="fas fa-file-excel"></i> Import from Excel</a>
|
||
<button type="button" @click="createTicketData = { name: '', description: '', status_id: '', priority_id: '', start_date: '', due_date: '' }; createTicketOpen = true" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors shadow-sm"><i class="fas fa-plus"></i> New Ticket</button>
|
||
</div>
|
||
</div>
|
||
<form id="tickets-bulk-delete-form" method="post" action="{% url 'tickets-bulk-delete' %}">
|
||
{% csrf_token %}
|
||
<template x-for="id in selectedTicketIds" x-key="id">
|
||
<input type="hidden" name="ids" :value="id">
|
||
</template>
|
||
</form>
|
||
<div x-show="selectedTicketIds.length > 0" x-cloak class="mb-4 flex items-center gap-3 px-4 py-2.5 bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-lg">
|
||
<span class="text-sm font-medium text-primary-700 dark:text-primary-300">
|
||
<span x-text="selectedTicketIds.length"></span> selected
|
||
</span>
|
||
<button type="button" @click="confirmAction({title:'Delete Tickets', message:'Delete ' + selectedTicketIds.length + ' ticket(s)?', confirmText:'Delete', danger:true, onConfirm:function(){ document.getElementById('tickets-bulk-delete-form').submit() }})" class="dk-btn dk-btn-danger text-xs px-3 py-1">
|
||
<i class="fas fa-trash mr-1"></i> Delete Selected
|
||
</button>
|
||
<button type="button" @click="selectedTicketIds = []; selectAllTickets = false" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 ml-auto">
|
||
<i class="fas fa-times mr-1"></i> Clear selection
|
||
</button>
|
||
</div>
|
||
<div class="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 w-10">
|
||
<input type="checkbox" @change="selectAllTickets = !selectAllTickets; selectedTicketIds = selectAllTickets ? Array.from(document.querySelectorAll('.ticket-checkbox')).map(cb => cb.value) : []" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
||
</th>
|
||
{% if "uuid" in tickets_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">UUID</th>{% endif %}
|
||
{% if "name" in tickets_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 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 dark:text-gray-400 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 dark:text-gray-400 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 dark:text-gray-400 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 dark:text-gray-400 uppercase tracking-wider">Start Date</th>{% endif %}
|
||
{% if "due_date" in tickets_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Due Date</th>{% endif %}
|
||
{% if "created_at" in tickets_columns %}<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Created</th>{% endif %}
|
||
<th class="px-4 py-3 text-left 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-600">
|
||
{% for t in tickets %}
|
||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30" :class="selectedTicketIds.includes('{{ t.id }}') && 'bg-blue-50 dark:bg-blue-900/10'">
|
||
<td class="px-4 py-3">
|
||
<input type="checkbox" class="ticket-checkbox rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" :value="'{{ t.id }}'" x-model="selectedTicketIds">
|
||
</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="inline-flex items-center 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="inline-flex items-center 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 dark:text-blue-400 border-2 border-white dark:border-gray-700" title="{{ a.get_full_name|default:a.username }}">{{ a.get_full_name|default:a.username|first|upper }}</div>{% endfor %}{% if t.assignees.all|length > 3 %}<div class="w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center text-[10px] text-gray-500 border-2 border-white dark:border-gray-700">+{{ t.assignees.all|length|add:"-3" }}</div>{% endif %}</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 %}
|
||
<td class="px-4 py-3"><div class="flex items-center gap-1.5"><button type="button" @click="editTicketData = {id: '{{ t.id }}', name: '{{ t.name|escapejs }}', description: '{{ t.description|escapejs }}', status_id: '{{ t.status_id }}', priority_id: '{{ t.priority_id|default:'' }}', start_date: '{{ t.start_date|date:'Y-m-d' }}', due_date: '{{ t.due_date|date:'Y-m-d' }}'}; editTicketOpen = true" class="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors shadow-sm">Edit</button><form method="post" action="{% url 'ticket-delete' t.id %}" class="inline">{% csrf_token %}<button type="button" @click="confirmAction({title:'Delete Ticket', message:'Delete this ticket?', 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">Delete</button></form></div></td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr><td colspan="11" 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-600 flex items-center justify-between">
|
||
<p class="text-xs text-gray-500">Showing {{ tickets.start_index }}–{{ tickets.end_index }} of {{ tickets.paginator.count }}</p>
|
||
<div class="flex items-center gap-1">
|
||
<form method="get" style="display:inline">
|
||
{% if tickets_search %}<input type="hidden" name="tickets_search" value="{{ tickets_search }}">{% endif %}
|
||
{% if tickets_status %}<input type="hidden" name="tickets_status" value="{{ tickets_status }}">{% endif %}
|
||
{% if tickets_priority %}<input type="hidden" name="tickets_priority" value="{{ tickets_priority }}">{% endif %}
|
||
<select name="tickets_per_page" onchange="this.form.submit()" class="text-xs border border-gray-200 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-950">
|
||
<option value="10" {% if tickets_per_page == 10 %}selected{% endif %}>10</option>
|
||
<option value="20" {% if tickets_per_page == 20 %}selected{% endif %}>20</option>
|
||
<option value="50" {% if tickets_per_page == 50 %}selected{% endif %}>50</option>
|
||
</select>
|
||
</form>
|
||
<div class="flex items-center gap-1 ml-2">
|
||
{% if tickets.has_previous %}
|
||
<a href="?tickets_page=1{% if tickets_search %}&tickets_search={{ tickets_search }}{% endif %}{% if tickets_status %}&tickets_status={{ tickets_status }}{% endif %}{% if tickets_priority %}&tickets_priority={{ tickets_priority }}{% endif %}&tickets_per_page={{ tickets_per_page }}" class="px-2 py-1 text-xs border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">«</a>
|
||
<a href="?tickets_page={{ tickets.previous_page_number }}{% if tickets_search %}&tickets_search={{ tickets_search }}{% endif %}{% if tickets_status %}&tickets_status={{ tickets_status }}{% endif %}{% if tickets_priority %}&tickets_priority={{ tickets_priority }}{% endif %}&tickets_per_page={{ tickets_per_page }}" class="px-2 py-1 text-xs border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">‹</a>
|
||
{% endif %}
|
||
<span class="px-2 py-1 text-xs text-gray-500">Page {{ tickets.number }}/{{ tickets.paginator.num_pages }}</span>
|
||
{% if tickets.has_next %}
|
||
<a href="?tickets_page={{ tickets.next_page_number }}{% if tickets_search %}&tickets_search={{ tickets_search }}{% endif %}{% if tickets_status %}&tickets_status={{ tickets_status }}{% endif %}{% if tickets_priority %}&tickets_priority={{ tickets_priority }}{% endif %}&tickets_per_page={{ tickets_per_page }}" class="px-2 py-1 text-xs border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">›</a>
|
||
<a href="?tickets_page={{ tickets.paginator.num_pages }}{% if tickets_search %}&tickets_search={{ tickets_search }}{% endif %}{% if tickets_status %}&tickets_status={{ tickets_status }}{% endif %}{% if tickets_priority %}&tickets_priority={{ tickets_priority }}{% endif %}&tickets_per_page={{ tickets_per_page }}" class="px-2 py-1 text-xs border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">»</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div x-show="activeTab === 'notes'" x-cloak 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-600 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-2">
|
||
<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 notes..." 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-950 focus:outline-none focus:ring-2 focus:ring-blue-500 w-44">
|
||
</div>
|
||
<input type="hidden" name="notes_per_page" value="{{ notes_per_page }}">
|
||
{% if notes_search %}<a href="{% url 'project-edit' project.id %}" class="text-xs text-gray-500 hover:text-gray-700">Clear</a>{% endif %}
|
||
</form>
|
||
<button type="button" @click="createNoteData = { title: '', content: '' }; createNoteOpen = true" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors shadow-sm"><i class="fas fa-plus"></i> New Note</button>
|
||
</div>
|
||
</div>
|
||
<form id="notes-bulk-delete-form" method="post" action="{% url 'notes-bulk-delete' %}">
|
||
{% csrf_token %}
|
||
<template x-for="id in selectedNoteIds" x-key="id">
|
||
<input type="hidden" name="ids" :value="id">
|
||
</template>
|
||
</form>
|
||
<div x-show="selectedNoteIds.length > 0" x-cloak class="mb-4 flex items-center gap-3 px-4 py-2.5 bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-lg">
|
||
<span class="text-sm font-medium text-primary-700 dark:text-primary-300">
|
||
<span x-text="selectedNoteIds.length"></span> selected
|
||
</span>
|
||
<button type="button" @click="confirmAction({title:'Delete Notes', message:'Delete ' + selectedNoteIds.length + ' note(s)?', confirmText:'Delete', danger:true, onConfirm:function(){ document.getElementById('notes-bulk-delete-form').submit() }})" class="dk-btn dk-btn-danger text-xs px-3 py-1">
|
||
<i class="fas fa-trash mr-1"></i> Delete Selected
|
||
</button>
|
||
<button type="button" @click="selectedNoteIds = []; selectAllNotes = false" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 ml-auto">
|
||
<i class="fas fa-times mr-1"></i> Clear selection
|
||
</button>
|
||
</div>
|
||
<div class="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 w-10">
|
||
<input type="checkbox" @change="selectAllNotes = !selectAllNotes; selectedNoteIds = selectAllNotes ? Array.from(document.querySelectorAll('.note-checkbox')).map(cb => cb.value) : []" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
||
</th>
|
||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Title</th>
|
||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Date</th>
|
||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Content</th>
|
||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Created By</th>
|
||
<th class="px-4 py-3 text-left 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-600">
|
||
{% for n in notes %}
|
||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30" :class="selectedNoteIds.includes('{{ n.id }}') && 'bg-blue-50 dark:bg-blue-900/10'">
|
||
<td class="px-4 py-3">
|
||
<input type="checkbox" class="note-checkbox rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" :value="'{{ n.id }}'" x-model="selectedNoteIds">
|
||
</td>
|
||
<td class="px-4 py-3 font-medium text-gray-900 dark:text-white">{{ n.title }}</td>
|
||
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{{ n.note_date|date:"d/m/Y" }}</td>
|
||
<td class="px-4 py-3 text-gray-500 dark:text-gray-400 max-w-xs truncate">{{ n.content|striptags|truncatewords:20|default:"-" }}</td>
|
||
<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>
|
||
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||
<button type="button" @click="editNoteData = {id: '{{ n.id }}', title: '{{ n.title|escapejs }}', content: '{{ n.content|escapejs }}'}; editNoteOpen = true" 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-2">Edit</button>
|
||
<form method="post" action="{% url 'notes-bulk-delete' %}" class="inline">
|
||
{% csrf_token %}
|
||
<input type="hidden" name="ids" value="{{ n.id }}">
|
||
<button type="button" @click="confirmAction({title:'Delete Note', message:'Delete this note?', 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">Delete</button>
|
||
</form>
|
||
</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-600 flex items-center justify-between">
|
||
<p class="text-xs text-gray-500">Showing {{ notes.start_index }}–{{ notes.end_index }} of {{ notes.paginator.count }}</p>
|
||
<div class="flex items-center gap-1">
|
||
<form method="get" style="display:inline">
|
||
{% if notes_search %}<input type="hidden" name="notes_search" value="{{ notes_search }}">{% endif %}
|
||
<select name="notes_per_page" onchange="this.form.submit()" class="text-xs border border-gray-200 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-950">
|
||
<option value="10" {% if notes_per_page == 10 %}selected{% endif %}>10</option>
|
||
<option value="20" {% if notes_per_page == 20 %}selected{% endif %}>20</option>
|
||
<option value="50" {% if notes_per_page == 50 %}selected{% endif %}>50</option>
|
||
</select>
|
||
</form>
|
||
<div class="flex items-center gap-1 ml-2">
|
||
{% if notes.has_previous %}
|
||
<a href="?notes_page=1{% if notes_search %}¬es_search={{ notes_search }}{% endif %}¬es_per_page={{ notes_per_page }}" class="px-2 py-1 text-xs border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">«</a>
|
||
<a href="?notes_page={{ notes.previous_page_number }}{% if notes_search %}¬es_search={{ notes_search }}{% endif %}¬es_per_page={{ notes_per_page }}" class="px-2 py-1 text-xs border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">‹</a>
|
||
{% endif %}
|
||
<span class="px-2 py-1 text-xs text-gray-500">Page {{ notes.number }}/{{ notes.paginator.num_pages }}</span>
|
||
{% if notes.has_next %}
|
||
<a href="?notes_page={{ notes.next_page_number }}{% if notes_search %}¬es_search={{ notes_search }}{% endif %}¬es_per_page={{ notes_per_page }}" class="px-2 py-1 text-xs border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">›</a>
|
||
<a href="?notes_page={{ notes.paginator.num_pages }}{% if notes_search %}¬es_search={{ notes_search }}{% endif %}¬es_per_page={{ notes_per_page }}" class="px-2 py-1 text-xs border border-gray-200 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-800">»</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if project %}
|
||
{% include "components/external_dashboard_modal.html" with show_state="externalModalOpen" %}
|
||
|
||
<script>
|
||
function copyToClipboard(elementId, message) {
|
||
const element = document.getElementById(elementId);
|
||
element.select();
|
||
element.setSelectionRange(0, 99999);
|
||
navigator.clipboard.writeText(element.value).catch(function() {
|
||
document.execCommand('copy');
|
||
});
|
||
showToast(message, 'success');
|
||
}
|
||
</script>
|
||
{% endif %}
|
||
{% if project %}
|
||
<!-- Create Status Modal -->
|
||
<div x-cloak x-show="createStatusOpen" class="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto">
|
||
<div class="fixed inset-0 bg-black/50" @click="createStatusOpen = 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">
|
||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Create Status</h3>
|
||
<button @click="createStatusOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||
<i class="fas fa-times text-lg"></i>
|
||
</button>
|
||
</div>
|
||
<form method="post" action="{% url 'ticket-status-create' %}">
|
||
{% csrf_token %}
|
||
<input type="hidden" name="project_id" value="{{ project.id }}">
|
||
<div class="px-8 pb-6 space-y-5">
|
||
<div class="grid grid-cols-2 gap-6">
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1.5">Status Name<span class="text-red-500">*</span></label>
|
||
<input type="text" name="name" x-model="createStatusData.name" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1.5">Sort Order<span class="text-red-500">*</span></label>
|
||
<input type="number" name="sort_order" x-model="createStatusData.sort_order" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-6">
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1.5">Color (Hex)<span class="text-red-500">*</span></label>
|
||
<div class="flex items-center gap-2">
|
||
<toolcool-color-picker popup-position="left" :color="createStatusData.color" @change="createStatusData.color = $event.detail.hex" class="shrink-0"></toolcool-color-picker>
|
||
<input type="text" name="color" x-model="createStatusData.color" required
|
||
class="w-full rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 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="flex items-center pt-7">
|
||
<label class="flex items-center gap-2 cursor-pointer">
|
||
<input type="checkbox" name="is_completed" x-model="createStatusData.is_completed" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||
<span class="text-sm font-semibold text-gray-900 dark:text-gray-300">Marks ticket as completed</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="px-8 py-5 flex justify-start gap-3">
|
||
<button type="submit" class="px-5 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors shadow-sm">Create status</button>
|
||
<button type="button" @click="createStatusOpen = false" class="px-5 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-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors shadow-sm">Cancel</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Add Member Modal -->
|
||
<div x-cloak x-show="createMemberOpen" class="fixed inset-0 z-50 flex items-center justify-center">
|
||
<div class="fixed inset-0 bg-black/50" @click="createMemberOpen = false"></div>
|
||
<div class="relative bg-white dark:bg-gray-900 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-md mx-4 sm:mx-auto my-auto z-10 overflow-visible">
|
||
|
||
<!-- Header -->
|
||
<div class="px-6 py-5 flex items-center justify-between">
|
||
<h3 class="text-base font-bold text-gray-900 dark:text-white">Attach User</h3>
|
||
<button type="button" @click="createMemberOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Body & Form -->
|
||
<form method="post" action="{% url 'project-member-create' %}">
|
||
{% csrf_token %}
|
||
<input type="hidden" name="project_id" value="{{ project.id }}">
|
||
|
||
<div class="px-6 pb-4">
|
||
<div class="flex shadow-sm rounded-lg relative" x-data="{
|
||
open: false,
|
||
search: '',
|
||
options: [
|
||
{% for u in users %}
|
||
{% if u.id not in member_ids %}
|
||
{ id: {{ u.id }}, name: '{{ u.get_full_name|default:u.username|escapejs }} (@{{ u.username|escapejs }})' },
|
||
{% endif %}
|
||
{% endfor %}
|
||
],
|
||
get filteredOptions() {
|
||
if (this.search === '') return this.options;
|
||
return this.options.filter(i => i.name.toLowerCase().includes(this.search.toLowerCase()));
|
||
}
|
||
}">
|
||
<input type="hidden" name="user_id" x-model="createMemberData.user_id" required>
|
||
|
||
<button type="button" @click="open = !open" @click.away="open = false" class="flex-1 flex items-center justify-between px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-l-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none hover:bg-gray-50 dark:hover:bg-gray-700">
|
||
<span x-text="createMemberData.user_id ? options.find(o => o.id == createMemberData.user_id)?.name : 'Select an option'" :class="!createMemberData.user_id && 'text-gray-500'"></span>
|
||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||
</button>
|
||
|
||
<!-- Dropdown Menu -->
|
||
<div x-cloak x-show="open" class="absolute left-0 right-11 top-full mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50 overflow-hidden">
|
||
<div class="p-2 border-b border-gray-100 dark:border-gray-700">
|
||
<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" x-model="search" placeholder="Search users..." class="w-full pl-8 pr-3 py-1.5 text-sm border border-gray-200 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-1 focus:ring-blue-500 text-gray-900 dark:text-white" @click.stop>
|
||
</div>
|
||
</div>
|
||
<ul class="max-h-48 overflow-y-auto py-1">
|
||
<template x-for="option in filteredOptions" :key="option.id">
|
||
<li @click="createMemberData.user_id = option.id; open = false; search = ''" class="px-3 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-blue-50 dark:hover:bg-blue-900/30 cursor-pointer" x-text="option.name"></li>
|
||
</template>
|
||
<li x-show="filteredOptions.length === 0" class="px-3 py-4 text-sm text-gray-500 text-center">No users found</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- Plus Button to Open Create User Modal -->
|
||
<button type="button" @click="createMemberOpen = false; setTimeout(() => { createUserOpen = true }, 100)" class="px-4 text-gray-500 border border-l-0 border-gray-200 dark:border-gray-600 rounded-r-lg bg-gray-50 dark:bg-gray-800/50 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center justify-center transition-colors">
|
||
<i class="fas fa-plus"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div class="px-6 pb-6 pt-2 flex flex-wrap gap-2.5">
|
||
<button type="submit" class="px-4 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors">Attach</button>
|
||
<button type="submit" name="create_another" value="1" class="px-4 py-1.5 text-sm font-medium text-gray-900 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">Attach & attach another</button>
|
||
<button type="button" @click="createMemberOpen = false" class="px-4 py-1.5 text-sm font-medium text-gray-900 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">Cancel</button>
|
||
</div>
|
||
</form>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Create User Modal -->
|
||
<div x-cloak x-show="createUserOpen" class="fixed inset-0 z-50 flex items-center justify-center">
|
||
<div class="fixed inset-0 bg-black/50" @click="createUserOpen = false"></div>
|
||
<div class="relative bg-white dark:bg-gray-900 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-md mx-4 sm:mx-auto my-auto z-10 max-h-[90vh] overflow-y-auto">
|
||
|
||
<!-- Header -->
|
||
<div class="px-6 py-5 flex items-center justify-between">
|
||
<h3 class="text-base font-bold text-gray-900 dark:text-white">Create</h3>
|
||
<button type="button" @click="createUserOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Body & Form -->
|
||
<form method="post" action="{% url 'user-create' %}?next={{ request.path }}">
|
||
{% csrf_token %}
|
||
|
||
<div class="px-6 pb-4 space-y-4">
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Name<span class="text-red-500">*</span></label>
|
||
<input type="text" name="name" required class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Email<span class="text-red-500">*</span></label>
|
||
<input type="email" name="email" required class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Email verified at</label>
|
||
<input type="datetime-local" name="email_verified_at" class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-400 dark:text-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Password<span class="text-red-500">*</span></label>
|
||
<input type="password" name="password" required class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1">Roles</label>
|
||
<select name="role" class="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500">
|
||
<option value="" disabled selected>Select an option</option>
|
||
<option value="Super Admin">Super Admin</option>
|
||
<option value="Admin User">Admin User</option>
|
||
<option value="Member User">Member User</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div class="px-6 pb-6 pt-2 flex flex-wrap gap-2.5">
|
||
<button type="submit" class="px-4 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors">Create</button>
|
||
<button type="button" @click="createUserOpen = false" class="px-4 py-1.5 text-sm font-medium text-gray-900 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">Cancel</button>
|
||
</div>
|
||
</form>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Edit Status Modal -->
|
||
<div x-cloak x-show="editStatusOpen" class="fixed inset-0 z-50 flex items-center justify-center">
|
||
<div class="fixed inset-0 bg-black/50" @click="editStatusOpen = 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-6 py-4 border-b border-gray-100 dark:border-gray-600 flex items-center justify-between">
|
||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Edit Status</h3>
|
||
<button @click="editStatusOpen = false" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
|
||
</div>
|
||
<form method="post" :action="'/ticket-statuses/' + editStatusData.id + '/edit/'">
|
||
{% csrf_token %}
|
||
<div class="px-6 py-5 space-y-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Name</label>
|
||
<input type="text" name="name" x-model="editStatusData.name" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Color (Hex)</label>
|
||
<div class="flex items-center gap-2">
|
||
<toolcool-color-picker popup-position="left" :color="editStatusData.color" @change="editStatusData.color = $event.detail.hex" class="shrink-0"></toolcool-color-picker>
|
||
<input type="text" name="color" x-model="editStatusData.color" required
|
||
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 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>
|
||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Sort Order</label>
|
||
<input type="number" name="sort_order" x-model="editStatusData.sort_order" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<input type="checkbox" name="is_completed" x-model="editStatusData.is_completed" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Marks ticket as completed</label>
|
||
</div>
|
||
</div>
|
||
<div class="px-6 py-4 border-t border-gray-100 dark:border-gray-600 bg-gray-50 dark:bg-gray-800/30 flex justify-end gap-3">
|
||
<button type="button" @click="editStatusOpen = 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">Cancel</button>
|
||
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-lg hover:bg-blue-700 transition-colors shadow-sm">Save Changes</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Create Note Modal -->
|
||
<div x-cloak x-show="createNoteOpen" class="fixed inset-0 z-50 flex items-center justify-center">
|
||
<div class="fixed inset-0 bg-black/50" @click="createNoteOpen = 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-6 py-4 border-b border-gray-100 dark:border-gray-600 flex items-center justify-between">
|
||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Create Note</h3>
|
||
<button @click="createNoteOpen = false" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
|
||
</div>
|
||
<form method="post" action="{% url 'project-note-create' %}" >
|
||
{% csrf_token %}
|
||
<input type="hidden" name="project_id" value="{{ project.id }}">
|
||
|
||
<div class="px-6 py-5 space-y-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Title</label>
|
||
<input type="text" name="title" x-model="createNoteData.title" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
</div>
|
||
<div class="prose-container">
|
||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Content</label>
|
||
{% include "components/rich_text_editor.html" with input_name="content" initial_value="" %}
|
||
</div>
|
||
</div>
|
||
<div class="px-8 py-5 flex justify-end gap-3">
|
||
<button type="button" @click="createNoteOpen = 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">Cancel</button>
|
||
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-lg hover:bg-blue-700 transition-colors shadow-sm">Save Note</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Edit Note Modal -->
|
||
<div x-cloak x-show="editNoteOpen" class="fixed inset-0 z-50 flex items-center justify-center">
|
||
<div class="fixed inset-0 bg-black/50" @click="editNoteOpen = 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">
|
||
<h3 class="text-xl font-bold text-gray-900 dark:text-white" x-text="'Edit ' + editNoteData.title">Edit Note</h3>
|
||
<button @click="editNoteOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||
<i class="fas fa-times text-lg"></i>
|
||
</button>
|
||
</div>
|
||
<form method="post" :action="'/project-notes/' + editNoteData.id + '/edit/'" >
|
||
{% csrf_token %}
|
||
|
||
<div class="px-8 pb-6 space-y-5">
|
||
<div class="grid grid-cols-1 gap-6">
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1.5">Note Title<span class="text-red-500">*</span></label>
|
||
<input type="text" name="title" x-model="editNoteData.title" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1.5">Content</label>
|
||
{% include "components/rich_text_editor.html" with input_name="content" alpine_model="editNoteData.content" %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="px-8 py-5 flex justify-start gap-3">
|
||
<button type="submit" class="px-5 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors shadow-sm">Save changes</button>
|
||
<button type="button" @click="editNoteOpen = false" class="px-5 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-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors shadow-sm">Cancel</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Create Epic Modal -->
|
||
<div x-cloak x-show="createEpicOpen" class="fixed inset-0 z-50 flex items-center justify-center">
|
||
<div class="fixed inset-0 bg-black/50" @click="createEpicOpen = 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">
|
||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Create Epic</h3>
|
||
<button @click="createEpicOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||
<i class="fas fa-times text-lg"></i>
|
||
</button>
|
||
</div>
|
||
<form method="post" action="{% url 'epic-create-project' project.id %}" >
|
||
{% csrf_token %}
|
||
<div class="px-8 pb-6 space-y-5">
|
||
<div class="grid grid-cols-2 gap-6">
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1.5">Epic Name<span class="text-red-500">*</span></label>
|
||
<input type="text" name="name" x-model="createEpicData.name" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1.5">Sort Order</label>
|
||
<input type="number" name="sort_order" x-model="createEpicData.sort_order" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
<span class="text-xs text-gray-500 dark:text-gray-400 mt-1 block">Lower numbers appear first</span>
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-6">
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1.5">Start Date</label>
|
||
<input type="date" name="start_date" x-model="createEpicData.start_date" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1.5">End Date</label>
|
||
<input type="date" name="end_date" x-model="createEpicData.end_date" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1.5">Description</label>
|
||
{% include "components/rich_text_editor.html" with input_name="description" alpine_model="createEpicData.description" %}
|
||
</div>
|
||
</div>
|
||
<div class="px-8 py-5 flex justify-start gap-3">
|
||
<button type="submit" class="px-5 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors shadow-sm">Create Epic</button>
|
||
<button type="button" @click="createEpicOpen = false" class="px-5 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-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors shadow-sm">Cancel</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Edit Epic Modal -->
|
||
{% include "components/edit_epic_modal.html" %}
|
||
|
||
<!-- Create Ticket Modal -->
|
||
{% include "components/create_ticket_modal.html" %}
|
||
|
||
<!-- Edit Ticket Modal -->
|
||
<div x-cloak x-show="editTicketOpen" class="fixed inset-0 z-50 flex items-center justify-center">
|
||
<div class="fixed inset-0 bg-black/50" @click="editTicketOpen = 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">
|
||
<h3 class="text-xl font-bold text-gray-900 dark:text-white" x-text="'Edit ' + editTicketData.name">Edit Ticket</h3>
|
||
<button @click="editTicketOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||
<i class="fas fa-times text-lg"></i>
|
||
</button>
|
||
</div>
|
||
<form method="post" :action="'/tickets/' + editTicketData.id + '/edit/'" >
|
||
{% csrf_token %}
|
||
<div class="px-8 pb-6 space-y-5">
|
||
<div class="grid grid-cols-2 gap-6">
|
||
<div class="col-span-2">
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1.5">Ticket Name<span class="text-red-500">*</span></label>
|
||
<input type="text" name="name" x-model="editTicketData.name" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-6">
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1.5">Status</label>
|
||
<select name="status_id" x-model="editTicketData.status_id" required class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
{% for s in statuses %}
|
||
<option value="{{ s.id }}">{{ s.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1.5">Priority</label>
|
||
<select name="priority_id" x-model="editTicketData.priority_id" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
<option value="">— None —</option>
|
||
{% for p in priorities %}
|
||
<option value="{{ p.id }}">{{ p.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-6">
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1.5">Start Date</label>
|
||
<input type="date" name="start_date" x-model="editTicketData.start_date" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1.5">Due Date</label>
|
||
<input type="date" name="due_date" x-model="editTicketData.due_date" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-900 dark:text-gray-300 mb-1.5">Description</label>
|
||
{% include "components/rich_text_editor.html" with input_name="description" alpine_model="editTicketData.description" %}
|
||
</div>
|
||
</div>
|
||
<div class="px-8 py-5 flex justify-start gap-3">
|
||
<button type="submit" class="px-5 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors shadow-sm">Save changes</button>
|
||
<button type="button" @click="editTicketOpen = false" class="px-5 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-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors shadow-sm">Cancel</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
{% endif %}
|
||
{% endblock %}
|
||
{% block extra_scripts %}
|
||
{% endblock %}
|