V.1.2 - Improved Secrurity, Better Ticket Handling
This commit is contained in:
1 parent
cb5d091be2
commit
3136c81533
16 files changed
+879
-738
No files matched your search
+143
-41
@@ -9,21 +9,130 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const statOpen = document.getElementById('open-tickets');
|
||||
const statInProgress = document.getElementById('in-progress-tickets');
|
||||
const statClosed = document.getElementById('closed-tickets');
|
||||
const statTotalTime = document.getElementById('time-tickets'); // Neu: Gesamtzeit Anzeige
|
||||
const statTotalTime = document.getElementById('time-tickets');
|
||||
|
||||
// Accordion elements
|
||||
const closedToggle = document.getElementById('closed-toggle');
|
||||
const closedAccordion = document.getElementById('closed-accordion');
|
||||
const closedCount = document.getElementById('closed-count');
|
||||
|
||||
// PFP elements
|
||||
const updatePfpBtn = document.getElementById('update-pfp-btn');
|
||||
const pfpModal = document.getElementById('update-pfp-modal');
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const fileInput = document.getElementById('pfp-file-input');
|
||||
const uploadPfpBtn = document.getElementById('upload-pfp-btn');
|
||||
|
||||
let selectedPfpFile = null;
|
||||
let accordionOpen = false;
|
||||
|
||||
searchInput.addEventListener('input', refreshTickets);
|
||||
statusFilter.addEventListener('change', refreshTickets);
|
||||
priorityFilter.addEventListener('change', refreshTickets);
|
||||
categoryFilter.addEventListener('change', refreshTickets);
|
||||
|
||||
// Helper: Status/Priority zu "Title-Case-With-Dash"
|
||||
function toTitleCaseWithDash(str) {
|
||||
return str
|
||||
.split(' ')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join('-');
|
||||
// --- XSS Protection ---
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function toTitleCaseWithDash(str) {
|
||||
return str.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join('-');
|
||||
}
|
||||
|
||||
// --- Dynamic Categories ---
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const res = await fetch('/api/categories');
|
||||
const categories = await res.json();
|
||||
if (categoryFilter) {
|
||||
categoryFilter.innerHTML = '<option value="all">All Categories</option>' +
|
||||
categories.map(c => `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`).join('');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load categories:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Accordion ---
|
||||
if (closedToggle) {
|
||||
closedToggle.addEventListener('click', () => {
|
||||
accordionOpen = !accordionOpen;
|
||||
closedAccordion.style.display = accordionOpen ? 'block' : 'none';
|
||||
closedToggle.classList.toggle('open', accordionOpen);
|
||||
});
|
||||
}
|
||||
|
||||
// --- PFP Modal ---
|
||||
function openPfpModal() {
|
||||
if (pfpModal) {
|
||||
const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
|
||||
pfpModal.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
if (scrollBarWidth > 0) document.body.style.paddingRight = scrollBarWidth + 'px';
|
||||
}
|
||||
}
|
||||
function closePfpModal() {
|
||||
if (pfpModal) {
|
||||
pfpModal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
document.documentElement.style.overflow = '';
|
||||
document.body.style.paddingRight = '';
|
||||
}
|
||||
}
|
||||
if (updatePfpBtn) updatePfpBtn.addEventListener('click', openPfpModal);
|
||||
if (pfpModal) {
|
||||
window.addEventListener('click', (e) => { if (e.target === pfpModal) closePfpModal(); });
|
||||
window.addEventListener('keydown', (e) => { if (e.key === 'Escape' && pfpModal.style.display === 'flex') closePfpModal(); });
|
||||
}
|
||||
|
||||
// --- Drag & Drop ---
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('click', () => fileInput.click());
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.style.borderColor = '#10b981'; });
|
||||
dropZone.addEventListener('dragleave', () => { dropZone.style.borderColor = '#4b5563'; });
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.style.borderColor = '#4b5563';
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file && file.type.startsWith('image/')) {
|
||||
selectedPfpFile = file;
|
||||
dropZone.querySelector('p').textContent = file.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file && file.type.startsWith('image/')) {
|
||||
selectedPfpFile = file;
|
||||
dropZone.querySelector('p').textContent = file.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (uploadPfpBtn) {
|
||||
uploadPfpBtn.addEventListener('click', async () => {
|
||||
if (!selectedPfpFile) return alert('Please select an image first.');
|
||||
const formData = new FormData();
|
||||
formData.append('pfp', selectedPfpFile, 'admin.jpg');
|
||||
try {
|
||||
const res = await fetch('/api/upload-admin-pfp', { method: 'POST', body: formData });
|
||||
if (!res.ok) throw new Error('Upload failed');
|
||||
alert('Admin profile picture updated!');
|
||||
closePfpModal();
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert('Error uploading profile picture');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- API ---
|
||||
async function fetchTickets() {
|
||||
try {
|
||||
const res = await fetch('/api/tickets');
|
||||
@@ -39,7 +148,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const res = await fetch(`/api/tickets/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ actor: 'Admin' })
|
||||
body: JSON.stringify({ actor: 'Admin' }),
|
||||
});
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
@@ -59,33 +168,33 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const category = categoryFilter.value;
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
|
||||
const filtered = tickets.filter(t =>
|
||||
(status === 'all' || t.status === status) &&
|
||||
(priority === 'all' || t.priority === priority) &&
|
||||
(category === 'all' || t.category === category) &&
|
||||
(t.title.toLowerCase().includes(searchTerm) ||
|
||||
t.description.toLowerCase().includes(searchTerm) ||
|
||||
t.requester.toLowerCase().includes(searchTerm))
|
||||
).sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||
const filtered = tickets
|
||||
.filter(t =>
|
||||
(status === 'all' || t.status === status) &&
|
||||
(priority === 'all' || t.priority === priority) &&
|
||||
(category === 'all' || t.category === category) &&
|
||||
(t.title.toLowerCase().includes(searchTerm) ||
|
||||
t.description.toLowerCase().includes(searchTerm) ||
|
||||
t.requester.toLowerCase().includes(searchTerm))
|
||||
)
|
||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||
|
||||
const openRows = [];
|
||||
const closedRows = [];
|
||||
|
||||
filtered.forEach(ticket => {
|
||||
const shortTitle = ticket.title.length > 50 ? ticket.title.slice(0, 50) + '...' : ticket.title;
|
||||
|
||||
// Wandle Status und Priority korrekt in Klassen um
|
||||
const statusClass = toTitleCaseWithDash(ticket.status || 'Unknown');
|
||||
const priorityClass = toTitleCaseWithDash(ticket.priority || 'None');
|
||||
|
||||
const rowHTML = `
|
||||
<tr>
|
||||
<td>${ticket.id}</td>
|
||||
<td class="ticket-title">${shortTitle}</td>
|
||||
<td>${ticket.requester}</td>
|
||||
<td>${ticket.category || '-'}</td>
|
||||
<td><span class="status-badge status-${statusClass}">${ticket.status || 'Unknown'}</span></td>
|
||||
<td><span class="priority-badge priority-${priorityClass}">${ticket.priority || 'None'}</span></td>
|
||||
<td class="ticket-title">${escapeHtml(shortTitle)}</td>
|
||||
<td>${escapeHtml(ticket.requester)}</td>
|
||||
<td>${escapeHtml(ticket.category) || '-'}</td>
|
||||
<td><span class="status-badge status-${statusClass}">${escapeHtml(ticket.status) || 'Unknown'}</span></td>
|
||||
<td><span class="priority-badge priority-${priorityClass}">${escapeHtml(ticket.priority) || 'None'}</span></td>
|
||||
<td>${formatDate(ticket.created_at)}</td>
|
||||
<td>${ticket.total_time} min</td>
|
||||
<td class="table-actions">
|
||||
@@ -94,31 +203,28 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
if (ticket.status === 'Closed') {
|
||||
closedRows.push(rowHTML);
|
||||
} else {
|
||||
openRows.push(rowHTML);
|
||||
}
|
||||
if (ticket.status === 'Closed') closedRows.push(rowHTML);
|
||||
else openRows.push(rowHTML);
|
||||
});
|
||||
|
||||
ticketsBody.innerHTML = openRows.length
|
||||
? openRows.join('')
|
||||
: `<tr><td colspan="8" class="empty-table">No open tickets found</td></tr>`;
|
||||
: `<tr><td colspan="9" class="empty-table">No open tickets found</td></tr>`;
|
||||
|
||||
if (closedTicketsBody) {
|
||||
closedTicketsBody.innerHTML = closedRows.length
|
||||
? closedRows.join('')
|
||||
: `<tr><td colspan="8" class="empty-table">No closed tickets</td></tr>`;
|
||||
: `<tr><td colspan="9" class="empty-table">No closed tickets</td></tr>`;
|
||||
}
|
||||
|
||||
// Event listeners an Buttons hängen
|
||||
[...document.querySelectorAll('.view-btn')].forEach(btn =>
|
||||
if (closedCount) closedCount.textContent = closedRows.length;
|
||||
|
||||
document.querySelectorAll('.view-btn').forEach(btn =>
|
||||
btn.addEventListener('click', () => {
|
||||
window.location.href = `/ticketview/ticket-view-admin.html?id=${btn.dataset.id}`;
|
||||
})
|
||||
);
|
||||
|
||||
[...document.querySelectorAll('.delete-btn')].forEach(btn =>
|
||||
document.querySelectorAll('.delete-btn').forEach(btn =>
|
||||
btn.addEventListener('click', () => {
|
||||
if (confirm('Are you sure you want to delete this ticket?')) {
|
||||
deleteTicket(btn.dataset.id).then(refreshTickets);
|
||||
@@ -132,19 +238,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
statOpen.textContent = tickets.filter(t => t.status === 'Open').length;
|
||||
statInProgress.textContent = tickets.filter(t => t.status === 'In Progress').length;
|
||||
statClosed.textContent = tickets.filter(t => t.status === 'Closed').length;
|
||||
|
||||
// Gesamtzeit aller Tickets summieren (total_time in Minuten)
|
||||
const totalTime = tickets.reduce((sum, ticket) => sum + (ticket.total_time || 0), 0);
|
||||
const totalTime = tickets.reduce((sum, t) => sum + (t.total_time || 0), 0);
|
||||
statTotalTime.textContent = `${totalTime} min`;
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
return new Date(date).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
refreshTickets();
|
||||
loadCategories().then(() => refreshTickets());
|
||||
});
|
||||
Reference in new issue
Block a user