Files
IT_Ticket_System/app/templates/admin/tickets.html
T

363 lines
16 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 %}All Tickets{% endblock %}
{% block page_title %}All Tickets{% endblock %}
{% block content %}
<!-- Filters -->
<div class="card mb-4">
<div class="card-body">
<form method="GET" class="row g-2 align-items-end">
<div class="col-md-3">
<label class="form-label">Search</label>
<input type="text" class="form-control" name="q" value="{{ search }}"
placeholder="Title, ticket #, submitter, assignee, comments…"/>
</div>
<div class="col-md-2">
<label class="form-label">Status</label>
<select class="form-select" name="status">
<option value="">All Statuses</option>
{% for s in ['open','in_progress','pending','resolved','closed'] %}
<option value="{{ s }}" {% if s == status %}selected{% endif %}>{{ s.replace('_',' ').title() }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label class="form-label">Priority</label>
<select class="form-select" name="priority">
<option value="">All Priorities</option>
{% for p in ['low','medium','high','critical'] %}
<option value="{{ p }}" {% if p == priority %}selected{% endif %}>{{ p.title() }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label class="form-label">Assignment</label>
<select class="form-select" name="assigned">
<option value="">All</option>
<option value="me" {% if assigned == 'me' %}selected{% endif %}>Assigned to Me</option>
<option value="unassigned" {% if assigned == 'unassigned' %}selected{% endif %}>Unassigned</option>
</select>
</div>
<div class="col-md-3 d-flex gap-2">
<button type="submit" class="btn btn-primary flex-fill"><i class="bi bi-search me-1"></i>Filter</button>
<a href="{{ url_for('admin.all_tickets') }}" class="btn btn-secondary" title="Clear filters"><i class="bi bi-x-lg"></i></a>
<a href="{{ url_for('admin.export_tickets', status=status, priority=priority, assigned=assigned, q=search) }}"
class="btn btn-secondary" title="Export current results to CSV">
<i class="bi bi-download me-1"></i>CSV
</a>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-header d-flex align-items-center justify-content-between">
<div>
<i class="bi bi-collection me-2"></i>Tickets
<span style="font-size:12px;color:var(--muted);font-family:'Space Mono',monospace;">({{ tickets.total }})</span>
</div>
<a href="{{ url_for('tickets.create_ticket_behalf') }}" class="btn btn-primary btn-sm">
<i class="bi bi-person-plus me-1"></i>File on Behalf
</a>
</div>
<div class="card-body p-0">
{% if tickets.items %}
<table class="table mb-0">
<thead>
<tr>
<th style="width:36px;">
<input type="checkbox" id="select-all" title="Select all"
style="accent-color:var(--accent);width:15px;height:15px;cursor:pointer;"/>
</th>
<th>Ticket #</th>
<th>Title</th>
<th>Category</th>
<th>Status</th>
<th>Priority</th>
<th>Submitted By</th>
<th>Assigned To</th>
<th>Date</th>
<th></th>
</tr>
</thead>
<tbody>
{% for t in tickets.items %}
<tr>
<td>
<input type="checkbox" class="ticket-cb" value="{{ t.id }}"
style="accent-color:var(--accent);width:15px;height:15px;cursor:pointer;"/>
</td>
<td><a href="{{ url_for('tickets.ticket_detail', ticket_id=t.id) }}" class="mono" style="font-size:11px;color:var(--accent3);">{{ t.ticket_number }}</a></td>
<td style="font-size:13px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ t.title }}</td>
<td style="font-size:12px;color:var(--muted);">{{ t.category.replace('_',' ').title() }}</td>
<td><span class="badge badge-{{ t.status }}">{{ t.status.replace('_',' ').upper() }}</span></td>
<td><span class="badge badge-{{ t.priority }}">{{ t.priority.upper() }}</span></td>
<td style="font-size:13px;">{{ t.creator.full_name }}</td>
<td style="font-size:13px;color:var(--muted);">{{ t.assignee.full_name if t.assignee else '—' }}</td>
<td style="font-size:11px;color:var(--muted);">{{ t.created_at | localtime("%b %d") }}</td>
<td>
<div class="d-flex gap-1">
<a href="{{ url_for('tickets.ticket_detail', ticket_id=t.id) }}" class="btn btn-secondary btn-sm"><i class="bi bi-eye"></i></a>
{% if current_user.is_admin %}
<button type="button"
class="btn btn-sm delete-ticket-btn"
style="background:#fee2e2;color:#dc2626;border:1px solid #fca5a5;"
data-ticket-id="{{ t.id }}"
data-ticket-number="{{ t.ticket_number }}"
data-ticket-title="{{ t.title | e }}"
title="Delete ticket">
<i class="bi bi-trash"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if tickets.pages > 1 %}
<div class="d-flex justify-content-center py-3">
<nav><ul class="pagination mb-0">
{% if tickets.has_prev %}
<li class="page-item"><a class="page-link" href="{{ url_for('admin.all_tickets', page=tickets.prev_num, status=status, priority=priority, assigned=assigned, q=search) }}"></a></li>
{% endif %}
{% for p in tickets.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if p %}
<li class="page-item {% if p==tickets.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('admin.all_tickets', page=p, status=status, priority=priority, assigned=assigned, q=search) }}">{{ p }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link"></span></li>
{% endif %}
{% endfor %}
{% if tickets.has_next %}
<li class="page-item"><a class="page-link" href="{{ url_for('admin.all_tickets', page=tickets.next_num, status=status, priority=priority, assigned=assigned, q=search) }}"></a></li>
{% endif %}
</ul></nav>
</div>
{% endif %}
{% else %}
<div class="p-5 text-center" style="color:var(--muted);">
<i class="bi bi-inbox" style="font-size:40px;display:block;margin-bottom:12px;"></i>No tickets match the current filters.
</div>
{% endif %}
</div>
</div>
<!-- ── Single-ticket delete form (hidden, submitted via JS) ────────────────── -->
{% if current_user.is_admin %}
<form id="single-delete-form" method="POST" action="" style="display:none;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
</form>
<!-- ── Confirmation modal for delete actions ──────────────────────────────── -->
<div id="delete-modal-overlay"
style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);
z-index:2000;align-items:center;justify-content:center;">
<div style="background:var(--card-bg,#fff);border-radius:12px;padding:28px 32px;
max-width:480px;width:90%;box-shadow:0 16px 48px rgba(0,0,0,.25);">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
<span style="background:#fee2e2;border-radius:50%;width:40px;height:40px;
display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="bi bi-exclamation-triangle-fill" style="color:#dc2626;font-size:18px;"></i>
</span>
<h5 id="modal-title" style="margin:0;font-size:16px;font-weight:700;">Delete Ticket</h5>
</div>
<p id="modal-body" style="font-size:14px;color:var(--muted,#64748b);margin-bottom:24px;line-height:1.6;"></p>
<div class="d-flex gap-2 justify-content-end">
<button type="button" id="modal-cancel-btn" class="btn btn-secondary btn-sm">Cancel</button>
<button type="button" id="modal-confirm-btn"
class="btn btn-sm" style="background:#dc2626;color:#fff;border:none;">
<i class="bi bi-trash me-1"></i>Delete Permanently
</button>
</div>
</div>
</div>
{% endif %}
<!-- Bulk action bar: fixed at bottom, hidden until checkboxes are ticked -->
<!-- NOTE: display is set entirely via JS — no inline display property here -->
<div id="bulk-action-bar"
style="position:fixed;bottom:24px;left:50%;transform:translateX(-50%);
background:#1e293b;color:#fff;border-radius:12px;padding:12px 20px;
box-shadow:0 8px 32px rgba(0,0,0,.3);z-index:1000;
align-items:center;gap:12px;min-width:420px;">
<span id="bulk-count" style="font-size:13px;font-weight:600;white-space:nowrap;"></span>
<div style="flex:1;"></div>
<form method="POST" action="{{ url_for('admin.bulk_ticket_action') }}" id="bulk-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="status" value="{{ status }}"/>
<input type="hidden" name="priority" value="{{ priority }}"/>
<input type="hidden" name="assigned" value="{{ assigned }}"/>
<input type="hidden" name="q" value="{{ search }}"/>
<div id="bulk-hidden-ids"></div>
<div class="d-flex gap-2">
<button type="submit" name="action" value="resolve"
class="btn btn-sm" style="background:#059669;color:#fff;border:none;">
<i class="bi bi-check2-circle me-1"></i>Resolve
</button>
<button type="submit" name="action" value="close"
class="btn btn-sm" style="background:#6b7280;color:#fff;border:none;">
<i class="bi bi-archive me-1"></i>Close
</button>
<button type="submit" name="action" value="assign_me"
class="btn btn-sm" style="background:#2563eb;color:#fff;border:none;">
<i class="bi bi-person-check me-1"></i>Assign to Me
</button>
<button type="submit" name="action" value="unassign"
class="btn btn-sm" style="color:#fff;border:1px solid #6b7280;background:transparent;">
<i class="bi bi-person-dash me-1"></i>Unassign
</button>
{% if current_user.is_admin %}
<button type="button" id="bulk-delete-btn"
class="btn btn-sm" style="background:#dc2626;color:#fff;border:none;">
<i class="bi bi-trash me-1"></i>Delete
</button>
{% endif %}
</div>
</form>
<button type="button" onclick="clearSelection()"
style="background:none;border:none;color:#9ca3af;font-size:20px;cursor:pointer;
line-height:1;padding:0 0 0 4px;flex-shrink:0;" title="Clear selection">×</button>
</div>
<!-- Script is AFTER all DOM elements so querySelectorAll finds the checkboxes -->
<script>
(function () {
const bar = document.getElementById('bulk-action-bar');
const countEl = document.getElementById('bulk-count');
const hiddenIds = document.getElementById('bulk-hidden-ids');
const selectAll = document.getElementById('select-all');
// Start hidden — JS controls visibility entirely
bar.style.display = 'none';
function getChecked() {
return Array.from(document.querySelectorAll('.ticket-cb:checked'));
}
function updateBar() {
const checked = getChecked();
if (checked.length === 0) {
bar.style.display = 'none';
return;
}
bar.style.display = 'flex';
countEl.textContent = `${checked.length} ticket${checked.length !== 1 ? 's' : ''} selected`;
hiddenIds.innerHTML = checked.map(cb =>
`<input type="hidden" name="ticket_ids" value="${cb.value}"/>`
).join('');
}
window.clearSelection = function () {
document.querySelectorAll('.ticket-cb').forEach(cb => cb.checked = false);
if (selectAll) { selectAll.checked = false; selectAll.indeterminate = false; }
updateBar();
};
if (selectAll) {
selectAll.addEventListener('change', () => {
document.querySelectorAll('.ticket-cb').forEach(cb => {
cb.checked = selectAll.checked;
});
updateBar();
});
}
document.querySelectorAll('.ticket-cb').forEach(cb => {
cb.addEventListener('change', () => {
const all = document.querySelectorAll('.ticket-cb');
const done = document.querySelectorAll('.ticket-cb:checked');
if (selectAll) {
selectAll.indeterminate = done.length > 0 && done.length < all.length;
selectAll.checked = done.length === all.length && all.length > 0;
}
updateBar();
});
});
// ── Delete modal ──────────────────────────────────────────────────────────
const overlay = document.getElementById('delete-modal-overlay');
const modalTitle = document.getElementById('modal-title');
const modalBody = document.getElementById('modal-body');
const cancelBtn = document.getElementById('modal-cancel-btn');
const confirmBtn = document.getElementById('modal-confirm-btn');
if (!overlay) return; // non-admin: modal not rendered
let pendingAction = null;
function showModal(title, body, onConfirm) {
modalTitle.textContent = title;
modalBody.innerHTML = body;
pendingAction = onConfirm;
overlay.style.display = 'flex';
}
function hideModal() {
overlay.style.display = 'none';
pendingAction = null;
}
cancelBtn.addEventListener('click', hideModal);
overlay.addEventListener('click', function (e) {
if (e.target === overlay) hideModal();
});
confirmBtn.addEventListener('click', function () {
if (typeof pendingAction === 'function') pendingAction();
hideModal();
});
// ── Single-ticket delete ──────────────────────────────────────────────────
document.querySelectorAll('.delete-ticket-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
const ticketId = this.dataset.ticketId;
const ticketNumber = this.dataset.ticketNumber;
const ticketTitle = this.dataset.ticketTitle;
showModal(
'Delete Ticket',
'This will <strong>permanently delete</strong> ticket ' +
'<code>' + ticketNumber + '</code> — <em>' + ticketTitle + '</em> ' +
'and all associated comments, attachments, and history.' +
'<br><br>This action <strong>cannot be undone</strong>.',
function () {
const form = document.getElementById('single-delete-form');
form.action = '/admin/tickets/' + ticketId + '/delete';
form.submit();
}
);
});
});
// ── Bulk delete ───────────────────────────────────────────────────────────
const bulkDeleteBtn = document.getElementById('bulk-delete-btn');
if (bulkDeleteBtn) {
bulkDeleteBtn.addEventListener('click', function () {
const checked = getChecked();
if (checked.length === 0) return;
const n = checked.length;
showModal(
'Delete Selected Tickets',
'This will <strong>permanently delete ' + n + ' ticket' + (n !== 1 ? 's' : '') + '</strong> ' +
'and all associated comments, attachments, and history.' +
'<br><br>This action <strong>cannot be undone</strong>.',
function () {
const form = document.getElementById('bulk-form');
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'action';
input.value = 'delete';
form.appendChild(input);
form.submit();
}
);
});
}
})();
</script>
{% endblock %}