236 lines
9.3 KiB
JavaScript
236 lines
9.3 KiB
JavaScript
import {
|
||
fetchTicket, fetchComments, fetchHistory, createComment, logTime,
|
||
formatDateTime, formatMinutes, getHistoryIcon, escapeHtml, linkify
|
||
} from './ticket-shared.js';
|
||
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
const ticketId = new URLSearchParams(window.location.search).get('id');
|
||
if (!ticketId) return (window.location.href = '../admin.html');
|
||
|
||
const statusSelect = document.getElementById('status-select');
|
||
const priorityInput = document.getElementById('priority-text');
|
||
const categorySelect = document.getElementById('category-select');
|
||
const commentForm = document.getElementById('comment-form');
|
||
const titleText = document.getElementById('ticket-title-text');
|
||
const titleInput = document.getElementById('ticket-title-input');
|
||
const descText = document.getElementById('ticket-description-text');
|
||
const descInput = document.getElementById('ticket-description-input');
|
||
const commentAuthorInput = document.getElementById('comment-author');
|
||
const totalTimeDisplay = document.getElementById('total-time');
|
||
const timeForm = document.getElementById('time-booking-form');
|
||
|
||
let originalStatus = '', originalPriority = '', originalTitle = '', originalDesc = '', originalCategory = '';
|
||
|
||
// Load categories dynamically
|
||
async function loadCategories() {
|
||
try {
|
||
const res = await fetch('/api/categories');
|
||
const categories = await res.json();
|
||
if (categorySelect) {
|
||
categorySelect.innerHTML = categories.map(c =>
|
||
`<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`
|
||
).join('');
|
||
}
|
||
} catch (err) {
|
||
console.error('Failed to load categories:', err);
|
||
}
|
||
}
|
||
|
||
async function loadPage() {
|
||
await loadCategories();
|
||
await Promise.all([loadTicketDetails(), loadComments(), loadHistory()]);
|
||
}
|
||
|
||
async function loadTicketDetails() {
|
||
const ticket = await fetchTicket(ticketId);
|
||
if (!ticket) return alert('Ticket not found');
|
||
|
||
titleText.textContent = ticket.title;
|
||
descText.textContent = ticket.description;
|
||
priorityInput.value = ticket.priority;
|
||
statusSelect.value = ticket.status;
|
||
categorySelect.value = ticket.category || '';
|
||
|
||
document.getElementById('ticket-id-val').textContent = ticket.id;
|
||
document.getElementById('ticket-created').textContent = formatDateTime(ticket.created_at);
|
||
document.getElementById('ticket-updated').textContent = formatDateTime(ticket.updated_at);
|
||
totalTimeDisplay.textContent = formatMinutes(typeof ticket.total_time === 'number' ? ticket.total_time : 0);
|
||
|
||
statusSelect.className = 'form-control status-' + (ticket.status || '').toLowerCase().replace(/\s+/g, '-');
|
||
|
||
originalStatus = ticket.status;
|
||
originalPriority = ticket.priority;
|
||
originalTitle = ticket.title;
|
||
originalDesc = ticket.description;
|
||
originalCategory = ticket.category || '';
|
||
}
|
||
|
||
async function updateTicket({ status, priority, title, description, category, actor = 'Admin' }) {
|
||
try {
|
||
const ticket = await fetchTicket(ticketId);
|
||
const requester = ticket.requester || '';
|
||
|
||
const res = await fetch(`/api/tickets/${ticketId}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ status, priority, title, description, category, author: actor, requester }),
|
||
});
|
||
if (!res.ok) throw new Error('Failed to update');
|
||
|
||
// Webhooks are handled server-side now — no need to send from here
|
||
alert('Ticket updated successfully');
|
||
|
||
titleText.textContent = title;
|
||
descText.textContent = description;
|
||
await loadPage();
|
||
} catch (err) {
|
||
console.error('Update failed:', err);
|
||
alert('Error updating ticket');
|
||
}
|
||
}
|
||
|
||
document.getElementById('update-ticket-btn').addEventListener('click', async () => {
|
||
const isEditingTitle = titleInput.style.display === 'block';
|
||
const isEditingDesc = descInput.style.display === 'block';
|
||
|
||
const newTitle = isEditingTitle ? titleInput.value.trim() : titleText.textContent;
|
||
const newDesc = isEditingDesc ? descInput.value.trim() : descText.textContent;
|
||
|
||
if (!confirm('Save changes?')) return;
|
||
|
||
await updateTicket({
|
||
status: statusSelect.value,
|
||
priority: priorityInput.value,
|
||
title: newTitle,
|
||
description: newDesc,
|
||
category: categorySelect.value,
|
||
});
|
||
|
||
titleInput.style.display = 'none';
|
||
titleText.style.display = 'block';
|
||
descInput.style.display = 'none';
|
||
descText.style.display = 'block';
|
||
});
|
||
|
||
document.getElementById('close-ticket-btn').addEventListener('click', async () => {
|
||
if (!confirm('Close this ticket?')) return;
|
||
await updateTicket({
|
||
status: 'Closed',
|
||
priority: priorityInput.value,
|
||
title: titleText.textContent,
|
||
description: descText.textContent,
|
||
category: categorySelect.value,
|
||
});
|
||
});
|
||
|
||
titleText.addEventListener('dblclick', () => {
|
||
titleInput.value = titleText.textContent;
|
||
titleText.style.display = 'none';
|
||
titleInput.style.display = 'block';
|
||
titleInput.focus();
|
||
});
|
||
|
||
descText.addEventListener('dblclick', () => {
|
||
descInput.value = descText.textContent;
|
||
descText.style.display = 'none';
|
||
descInput.style.display = 'block';
|
||
descInput.focus();
|
||
});
|
||
|
||
// Restore saved author name
|
||
const savedAuthor = localStorage.getItem('commentAuthor');
|
||
if (savedAuthor) commentAuthorInput.value = savedAuthor;
|
||
|
||
async function loadComments() {
|
||
const comments = await fetchComments(ticketId);
|
||
const container = document.getElementById('comments-container');
|
||
container.innerHTML = comments.map(comment => {
|
||
const role = comment.role || 'user';
|
||
const isAdmin = role === 'admin';
|
||
const avatarUrl = isAdmin ? '/assets/admin.jpg' : '/assets/user.jpg';
|
||
return `
|
||
<div class="comment ${isAdmin ? 'comment-admin' : ''}">
|
||
<img class="comment-avatar" src="${avatarUrl}" alt="${isAdmin ? 'Admin' : 'User'} Avatar">
|
||
<div class="comment-content">
|
||
<div class="comment-header">
|
||
<span class="comment-author">${escapeHtml(comment.author)}</span>
|
||
${isAdmin ? '<span class="comment-role-badge">Admin</span>' : ''}
|
||
<span class="comment-date">${formatDateTime(comment.date)}</span>
|
||
</div>
|
||
<div class="comment-text">${linkify(comment.content)}</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
async function loadHistory() {
|
||
const history = await fetchHistory(ticketId);
|
||
const container = document.getElementById('history-container');
|
||
container.innerHTML = history.map(h => `
|
||
<div class="history-item">
|
||
<div class="history-icon"><i class="fas fa-${getHistoryIcon(h.action)}"></i></div>
|
||
<div class="history-content">
|
||
<div class="history-action">${escapeHtml(h.action)}</div>
|
||
<div class="history-details">${escapeHtml(h.message || '')}</div>
|
||
<div class="history-meta">
|
||
<span>By ${escapeHtml(h.author || h.actor || 'unknown')}</span>
|
||
<span>${formatDateTime(h.date || h.timestamp)}</span>
|
||
</div>
|
||
</div>
|
||
</div>`).join('');
|
||
}
|
||
|
||
commentForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const author = commentAuthorInput.value.trim();
|
||
if (!author) return alert('Please enter your name');
|
||
const content = document.getElementById('comment-content').value.trim();
|
||
if (!content) return alert('Comment required');
|
||
|
||
try {
|
||
await createComment(ticketId, { author, role: 'admin', content, timestamp: new Date().toISOString() });
|
||
localStorage.setItem('commentAuthor', author);
|
||
document.getElementById('comment-content').value = '';
|
||
await loadComments();
|
||
await loadHistory();
|
||
} catch (err) {
|
||
console.error('Failed to add comment:', err);
|
||
alert('Failed to add comment');
|
||
}
|
||
});
|
||
|
||
timeForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const minutes = parseInt(document.getElementById('time-minutes').value);
|
||
const comment = document.getElementById('time-comment').value.trim();
|
||
const actor = commentAuthorInput.value || 'Admin';
|
||
|
||
if (isNaN(minutes) || minutes <= 0) return alert('Enter valid minutes');
|
||
|
||
try {
|
||
await logTime(ticketId, { minutes, comment, author: actor });
|
||
|
||
const timeComment = `⏱ Time booked: ${formatMinutes(minutes)}${comment ? ` – ${comment}` : ''}`;
|
||
await createComment(ticketId, { author: actor, role: 'admin', content: timeComment, timestamp: new Date().toISOString() });
|
||
|
||
document.getElementById('time-minutes').value = '';
|
||
document.getElementById('time-comment').value = '';
|
||
await loadPage();
|
||
} catch (err) {
|
||
console.error('Error saving time:', err);
|
||
alert('Error saving time');
|
||
}
|
||
});
|
||
|
||
// Tabs
|
||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
||
document.getElementById(`${btn.dataset.tab}-tab`).classList.add('active');
|
||
});
|
||
});
|
||
|
||
await loadPage();
|
||
}); |