this is bennys version bitch
This commit is contained in:
121
templates/ticket_detail.html
Normal file
121
templates/ticket_detail.html
Normal file
@@ -0,0 +1,121 @@
|
||||
TICKET_DETAIL_HTML = r"""{% extends "base.html" %}
|
||||
{% block title %}#{{ t.id }} — {{ t.title }} · {{ brand }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="grid lg:grid-cols-3 gap-6">
|
||||
<section class="lg:col-span-2 card p-5">
|
||||
<header class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">#{{ t.id }} · {{ t.title }}</h1>
|
||||
<div class="mt-1 flex flex-wrap items-center gap-2 text-xs text-white/60">
|
||||
<span class="tag">{{ t.status.replace('_',' ').title() }}</span>
|
||||
<span class="tag">Priority: {{ t.priority }}</span>
|
||||
{% for label in t.label_list() %}<span class="tag">{{ label }}</span>{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% if can_manage %}
|
||||
<a class="btn" href="{{ url_for('admin_edit_ticket', ticket_id=t.id) }}">Edit</a>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
<article class="prose prose-invert max-w-none mt-4">
|
||||
<p class="whitespace-pre-wrap">{{ t.description }}</p>
|
||||
</article>
|
||||
|
||||
<hr class="my-5 border-white/10" />
|
||||
|
||||
<h2 class="font-semibold">Comments</h2>
|
||||
<div id="comments" class="mt-3 space-y-3">
|
||||
{% for c in t.comments %}
|
||||
<div class="card p-3">
|
||||
<div class="text-sm"><b>{{ c.author_name }}</b> <span class="text-white/50">· {{ c.created_at.strftime('%Y-%m-%d %H:%M') }} UTC</span></div>
|
||||
<div class="mt-1 whitespace-pre-wrap">{{ c.body }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-white/60">No comments yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if user %}
|
||||
<div class="mt-4">
|
||||
<textarea id="commentBox" rows="4" class="w-full bg-black/40 border border-white/10 rounded-lg px-3 py-2 text-sm sm:text-base" placeholder="Write a comment…"></textarea>
|
||||
<div class="mt-2 flex justify-end"><button id="commentBtn" class="btn-accent btn-block sm:btn">Post Comment</button></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<aside class="card p-5">
|
||||
<h3 class="font-semibold">Assignee</h3>
|
||||
<p class="mt-1 text-white/80">{{ t.assignee_name or (t.assignee_id and ('<@' ~ t.assignee_id ~ '>')) or 'Unassigned' }}</p>
|
||||
|
||||
{% if can_manage %}
|
||||
<div class="mt-3">
|
||||
<label class="text-xs text-white/60">Discord ID</label>
|
||||
<input id="assigneeId" value="{{ t.assignee_id or '' }}" class="w-full bg-black/40 border border-white/10 rounded-lg px-2 py-1.5" />
|
||||
<label class="text-xs text-white/60 mt-2 block">Display Name</label>
|
||||
<input id="assigneeName" value="{{ t.assignee_name or '' }}" class="w-full bg-black/40 border border-white/10 rounded-lg px-2 py-1.5" />
|
||||
<button id="assignBtn" class="btn-accent mt-2 w-full sm:w-auto">Assign</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h3 class="font-semibold mt-6">Status</h3>
|
||||
<div class="mt-2 grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
{% for s in ['open','in_progress','done','cancelled'] %}
|
||||
<button class="btn {% if t.status==s %}border-white/60{% endif %} btn-block" data-status="{{s}}" {% if not can_update_status %}disabled{% endif %}>
|
||||
{{ s.replace('_',' ').title() }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-white/50 mt-6">
|
||||
<div>Created: {{ t.created_at.strftime('%Y-%m-%d %H:%M') }} UTC</div>
|
||||
<div>Updated: {{ t.updated_at.strftime('%Y-%m-%d %H:%M') }} UTC</div>
|
||||
<div>By: {{ t.created_by_name or t.created_by_id }}</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ticketId = {{ t.id }};
|
||||
const commentBtn = document.getElementById('commentBtn');
|
||||
const commentBox = document.getElementById('commentBox');
|
||||
const comments = document.getElementById('comments');
|
||||
const assignBtn = document.getElementById('assignBtn');
|
||||
const assigneeId = document.getElementById('assigneeId');
|
||||
const assigneeName = document.getElementById('assigneeName');
|
||||
|
||||
function esc(s){return (s||'').replaceAll('&','&').replaceAll('<','<').replaceAll('>','>');}
|
||||
|
||||
commentBtn?.addEventListener('click', async () => {
|
||||
const body = commentBox.value.trim();
|
||||
if(!body) return;
|
||||
const r = await fetch(`/api/tickets/${ticketId}/comment`, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({body})});
|
||||
const data = await r.json();
|
||||
if(!r.ok){ alert(data.error||'Failed'); return; }
|
||||
commentBox.value='';
|
||||
const c = data.comment;
|
||||
const el = document.createElement('div');
|
||||
el.className='card p-3';
|
||||
el.innerHTML = `<div class="text-sm"><b>${esc(c.author_name)}</b> <span class="text-white/50">· ${(new Date(c.created_at)).toISOString().slice(0,16).replace('T',' ')} UTC</span></div><div class="mt-1 whitespace-pre-wrap">${esc(c.body)}</div>`;
|
||||
comments.prepend(el);
|
||||
});
|
||||
|
||||
assignBtn?.addEventListener('click', async () => {
|
||||
const payload = { assignee_id: assigneeId.value.trim(), assignee_name: assigneeName.value.trim() };
|
||||
const r = await fetch(`/api/tickets/{{ t.id }}/assign`, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload)});
|
||||
const data = await r.json();
|
||||
if(!r.ok){ alert(data.error||'Failed'); return; }
|
||||
location.reload();
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-status]')?.forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const status = btn.dataset.status;
|
||||
const r = await fetch(`/api/tickets/${ticketId}/status`, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({status})});
|
||||
const data = await r.json();
|
||||
if(!r.ok){ alert(data.error||'Failed'); return; }
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
"""
|
||||
Reference in New Issue
Block a user