Dateien nach "public/ticketview" hochladen
This commit is contained in:
1 parent
58ceca14b3
commit
784b660402
5 files changed
+1085
No files matched your search
@@ -0,0 +1,122 @@
|
||||
/* ==== ADMIN COMMENT STYLING ==== */
|
||||
.comment.admin .comment-content {
|
||||
background-color: var(--color-admin-highlight);
|
||||
border-left: 4px solid var(--color-primary);
|
||||
}
|
||||
.comment.admin .comment-author::after {
|
||||
content: " (Admin)";
|
||||
color: var(--color-primary);
|
||||
font-weight: normal;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* ==== ACTION BUTTONS (nur Admin hat mehrere Buttons) ==== */
|
||||
.ticket-actions-bar {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn-secondary {
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.btn-secondary {
|
||||
background-color: var(--color-accent);
|
||||
color: white;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background-color: #1976d2;
|
||||
}
|
||||
|
||||
/* ==== CUSTOM ADMIN LAYOUT FIX ==== */
|
||||
.ticket-meta-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
@media (min-width: 600px) {
|
||||
.ticket-info-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
/* GRID ADMIN SIDE ORDER */
|
||||
|
||||
.ticket-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr); /* 3 equal columns */
|
||||
grid-template-rows: auto auto auto; /* 3 rows */
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* Requester: span all 3 columns on row 1 */
|
||||
.info-item.requester {
|
||||
grid-column: 1 / 4;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
|
||||
/* Description: span all 3 columns on row 2 */
|
||||
.info-item.description {
|
||||
grid-column: 1 / 4;
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
|
||||
/* Created, Updated, Time: row 3, columns 1-3 */
|
||||
.info-item.created {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 3 / 4;
|
||||
}
|
||||
.info-item.updated {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 3 / 4;
|
||||
}
|
||||
/* Time div isn’t .info-item, so select it directly by nth-child */
|
||||
.ticket-info-grid > div:nth-child(5) {
|
||||
grid-column: 3 / 4;
|
||||
grid-row: 3 / 4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Status, Priority, Category: row 4, columns 1-3 */
|
||||
.info-item.status {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 4 / 5;
|
||||
}
|
||||
.info-item.priority {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 4 / 5;
|
||||
}
|
||||
.info-item.category {
|
||||
grid-column: 3 / 4;
|
||||
grid-row: 4 / 5;
|
||||
}
|
||||
|
||||
.forms-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.compact-form {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.forms-row input,
|
||||
.forms-row textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.forms-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
import {
|
||||
fetchTicket, fetchComments, fetchHistory, createComment, logTime,
|
||||
formatDateTime, formatMinutes, getHistoryIcon
|
||||
} 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 webhookConfig = null;
|
||||
|
||||
// Fetch Webhook Configuration
|
||||
async function loadWebhookConfig() {
|
||||
try {
|
||||
const response = await fetch('/api/webhooks');
|
||||
if (response.ok) {
|
||||
webhookConfig = await response.json();
|
||||
console.log('Webhook configuration loaded:', webhookConfig.configured);
|
||||
} else {
|
||||
console.warn('Failed to load webhook configuration');
|
||||
webhookConfig = { admin: null, user: null, configured: { admin: false, user: false } };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading webhook configuration:', error);
|
||||
webhookConfig = { admin: null, user: null, configured: { admin: false, user: false } };
|
||||
}
|
||||
}
|
||||
|
||||
// Send Webhook Notification
|
||||
async function sendWebhook(message) {
|
||||
if (!webhookConfig?.user) {
|
||||
console.log('User webhook not configured, skipping notification');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(webhookConfig.user, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content: message })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error('User webhook failed:', res.statusText);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error sending user webhook:', err);
|
||||
}
|
||||
}
|
||||
|
||||
let originalStatus = '', originalPriority = '', originalTitle = '', originalDesc = '', originalCategory = '';
|
||||
|
||||
async function loadPage() {
|
||||
loadWebhookConfig().then(() => {
|
||||
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-requester').textContent = ticket.requester;
|
||||
document.getElementById('ticket-created').textContent = formatDateTime(ticket.created_at);
|
||||
document.getElementById('ticket-updated').textContent = formatDateTime(ticket.updated_at);
|
||||
const totalMins = typeof ticket.total_time === 'number' ? ticket.total_time : 0;
|
||||
totalTimeDisplay.textContent = formatMinutes(totalMins);
|
||||
|
||||
|
||||
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); // Fetch ticket to get the requester
|
||||
const requester = ticket.requester || ''; // Ensure requester is included in the update request
|
||||
|
||||
const res = await fetch(`/api/tickets/${ticketId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status, priority, title, description, category, actor, requester }) // Add requester
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Failed to update');
|
||||
alert('Ticket updated successfully');
|
||||
|
||||
if (status !== originalStatus) {
|
||||
await sendWebhook(`Ticket #${ticketId} status changed from "${originalStatus}" to "${status}"`);
|
||||
originalStatus = status;
|
||||
}
|
||||
|
||||
if (priority !== originalPriority) {
|
||||
await sendWebhook(`Ticket #${ticketId} priority changed from "${originalPriority}" to "${priority}"`);
|
||||
originalPriority = priority;
|
||||
}
|
||||
|
||||
if (title !== originalTitle) {
|
||||
await sendWebhook(`Ticket #${ticketId} title changed by ${actor}`);
|
||||
originalTitle = title;
|
||||
}
|
||||
|
||||
if (description !== originalDesc) {
|
||||
await sendWebhook(`Ticket #${ticketId} description changed by ${actor}`);
|
||||
originalDesc = description;
|
||||
}
|
||||
|
||||
if (category !== originalCategory) {
|
||||
await sendWebhook(`Ticket #${ticketId} category changed from "${originalCategory}" to "${category}"`);
|
||||
originalCategory = category;
|
||||
}
|
||||
|
||||
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;
|
||||
const newStatus = statusSelect.value;
|
||||
const newPriority = priorityInput.value;
|
||||
const newCategory = categorySelect.value;
|
||||
|
||||
const confirmed = confirm('Möchten Sie die Änderungen wirklich speichern?');
|
||||
if (!confirmed) return;
|
||||
|
||||
await updateTicket({ status: newStatus, priority: newPriority, title: newTitle, description: newDesc, category: newCategory });
|
||||
|
||||
titleInput.style.display = 'none';
|
||||
titleText.style.display = 'block';
|
||||
descInput.style.display = 'none';
|
||||
descText.style.display = 'block';
|
||||
});
|
||||
|
||||
document.getElementById('close-ticket-btn').addEventListener('click', async () => {
|
||||
const confirmed = confirm('Möchtest du das Ticket wirklich schließen?');
|
||||
if (!confirmed) return;
|
||||
|
||||
const newStatus = 'Closed';
|
||||
const newPriority = priorityInput.value;
|
||||
const newTitle = titleText.textContent;
|
||||
const newDesc = descText.textContent;
|
||||
const newCategory = categorySelect.value;
|
||||
|
||||
await updateTicket({ status: newStatus, priority: newPriority, title: newTitle, description: newDesc, category: newCategory });
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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">${comment.author}</span>
|
||||
${isAdmin ? '<span class="comment-role-badge">Admin</span>' : ''}
|
||||
<span class="comment-date">${formatDateTime(comment.date)}</span>
|
||||
</div>
|
||||
<div class="comment-text">${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">${h.action}</div>
|
||||
<div class="history-details">${h.message || ''}</div>
|
||||
<div class="history-meta">
|
||||
<span>By ${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('Bitte gib deinen Namen ein');
|
||||
const content = document.getElementById('comment-content').value.trim();
|
||||
if (!content) return alert('Comment required');
|
||||
|
||||
const role = 'admin';
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
try {
|
||||
await createComment(ticketId, { author, role, content, timestamp });
|
||||
await sendWebhook(`New comment added by ${author} (${role}): "${content}" on ticket #${ticketId}`);
|
||||
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('Gültige Zeit in Minuten eingeben');
|
||||
|
||||
try {
|
||||
// Save the time log
|
||||
await logTime(ticketId, { minutes, comment, author: actor });
|
||||
|
||||
// Also add it as a comment
|
||||
const timeComment = `⏱ Zeit gebucht: ${formatMinutes(minutes)}${comment ? ` – ${comment}` : ''}`;
|
||||
await createComment(ticketId, {
|
||||
author: actor,
|
||||
role: 'admin',
|
||||
content: timeComment,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Webhook notification
|
||||
await sendWebhook(`Time booked: ${formatMinutes(minutes)} by ${actor} on ticket #${ticketId}${comment ? ` – "${comment}"` : ''}`);
|
||||
|
||||
// Clear inputs and reload
|
||||
document.getElementById('time-minutes').value = '';
|
||||
document.getElementById('time-comment').value = '';
|
||||
await loadPage();
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Speichern der Zeit:', err);
|
||||
alert('Fehler beim Speichern');
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const tab = btn.dataset.tab;
|
||||
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(`${tab}-tab`).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
await loadPage();
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Ticket Details (Admin) - Ticket System</title>
|
||||
<link rel="icon" type="image/x-icon" href="/assets/favicon_ticket.ico">
|
||||
<link rel="stylesheet" href="ticket-view.css" />
|
||||
<link rel="stylesheet" href="admin-view.css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1><i class="fas fa-ticket-alt"></i> Ticket Details (Admin)</h1>
|
||||
<div class="user-actions">
|
||||
<a href="../admin.html" class="back-link">
|
||||
<i class="fas fa-arrow-left"></i> Back to Admin
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="ticket-details-layout">
|
||||
<!-- Ticket Info Section -->
|
||||
<section class="ticket-details">
|
||||
<div class="ticket-header">
|
||||
<div id="ticket-title-view">
|
||||
<h2 id="ticket-title-text">Loading...</h2>
|
||||
<input id="ticket-title-input" class="form-control" style="display: none" />
|
||||
</div>
|
||||
<div class="ticket-meta">
|
||||
<span class="ticket-id">#<span id="ticket-id-val">-</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ticket-info-grid">
|
||||
<div class="info-item requester">
|
||||
<h4>Requester</h4>
|
||||
<p id="ticket-requester">-</p>
|
||||
</div>
|
||||
|
||||
<div class="info-item description">
|
||||
<h3>Description</h3>
|
||||
<p id="ticket-description-text">Loading...</p>
|
||||
<textarea id="ticket-description-input" class="form-control" style="display: none"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="info-item created">
|
||||
<h4>Created</h4>
|
||||
<p id="ticket-created">-</p>
|
||||
</div>
|
||||
|
||||
<div class="info-item updated">
|
||||
<h4>Last Updated</h4>
|
||||
<p id="ticket-updated">-</p>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<h4>Time</h4>
|
||||
<p><span id="total-time">0</span></p>
|
||||
</div>
|
||||
|
||||
<div class="info-item status">
|
||||
<h4><label for="status-select">Status</label></h4>
|
||||
<select id="status-select" class="form-control">
|
||||
<option value="Open">Open</option>
|
||||
<option value="In Progress">In Progress</option>
|
||||
<option value="On Hold">On Hold</option>
|
||||
<option value="Closed">Closed</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="info-item priority">
|
||||
<h4><label for="priority-text">Priority</label></h4>
|
||||
<select id="priority-text" class="form-control">
|
||||
<option value="Low">Low</option>
|
||||
<option value="Medium">Medium</option>
|
||||
<option value="High">High</option>
|
||||
<option value="Urgent">Urgent</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="info-item category">
|
||||
<h4><label for="category-select">Category</label></h4>
|
||||
<select id="category-select" class="form-control">
|
||||
<option value="HELP">HELP</option>
|
||||
<option value="HELP MEE NOOOW">HELP MEE NOOOW</option>
|
||||
<option value="IT-Support">IT-Support</option>
|
||||
<option value="Suggestion...">Suggestion...</option>
|
||||
<option value="Add Book pls">Add Book pls</option>
|
||||
<option value="Honeyycloud!">Honeyycloud!</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ticket-actions-bar">
|
||||
<button class="btn-primary" id="close-ticket-btn">
|
||||
<i class="fas fa-check-circle"></i> Close Ticket
|
||||
</button>
|
||||
<button class="btn-secondary" id="update-ticket-btn" title="Update">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Ticket Activity Section -->
|
||||
<section class="ticket-activity">
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-tab="comments">Comments</button>
|
||||
<button class="tab-btn" data-tab="history">History</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="comments-tab">
|
||||
<div class="forms-row">
|
||||
<form id="comment-form" class="comment-form compact-form">
|
||||
<input type="text" id="comment-author" placeholder="Your name" required />
|
||||
<textarea id="comment-content" rows="2" placeholder="Add a comment..." required></textarea>
|
||||
<button class="btn-primary" type="submit">Post Comment</button>
|
||||
</form>
|
||||
|
||||
<form id="time-booking-form" class="comment-form compact-form">
|
||||
<input type="number" id="time-minutes" name="time-minutes" min="0" placeholder="Minutes" required />
|
||||
<textarea id="time-comment" name="time-comment" rows="2" placeholder="Optional comment"></textarea>
|
||||
<button class="btn-primary" type="submit">Add Time</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Comment Output -->
|
||||
<div id="comments-container"></div>
|
||||
|
||||
<!-- Time Logs -->
|
||||
<section id="time-logs" class="time-logs"></section>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="history-tab">
|
||||
<div id="history-container"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>Ticket System by Honey v1.1 © 2025</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script type="module" src="ticket-admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>My Ticket - Ticket System</title>
|
||||
<link rel="icon" type="image/x-icon" href="/assets/favicon_ticket.ico">
|
||||
<link rel="stylesheet" href="ticket-view.css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container user-view">
|
||||
<header class="header">
|
||||
<h1><i class="fas fa-ticket-alt"></i> My Ticket</h1>
|
||||
<div class="user-actions">
|
||||
<a href="../index.html" class="back-link"
|
||||
><i class="fas fa-arrow-left"></i> Back to Dashboard</a
|
||||
>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="ticket-details-layout">
|
||||
<section class="ticket-details">
|
||||
<div class="ticket-header">
|
||||
<h2 id="ticket-title-text">Loading...</h2>
|
||||
<div class="ticket-meta">
|
||||
<span class="ticket-id"
|
||||
>#<span id="ticket-id-val">-</span></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ticket-body">
|
||||
<!-- Requester first -->
|
||||
<div class="info-item requester">
|
||||
<h4>Requester</h4>
|
||||
<p id="ticket-requester">-</p>
|
||||
</div>
|
||||
|
||||
<!-- Then description -->
|
||||
<h3>Description</h3>
|
||||
<p id="ticket-description-text">Loading...</p>
|
||||
|
||||
<!-- Then grid info -->
|
||||
<div class="ticket-info-grid">
|
||||
<div class="info-item">
|
||||
<h4>Created</h4>
|
||||
<p id="ticket-created">-</p>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<h4>Last Updated</h4>
|
||||
<p id="ticket-updated">-</p>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<h4>Time</h4>
|
||||
<p><span id="total-time">0</span></p>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<h4>Status</h4>
|
||||
<span id="ticket-status-badge" class="ticket-status">Open</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<h4>Priority</h4>
|
||||
<span id="priority-text" class="ticket-priority">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<h4>Category</h4>
|
||||
<span id="ticket-category" class="ticket-category">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ticket-activity">
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-tab="comments">Comments</button>
|
||||
<button class="tab-btn" data-tab="history">History</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="comments-tab">
|
||||
<form id="comment-form" class="comment-form">
|
||||
<input
|
||||
type="text"
|
||||
id="comment-author"
|
||||
class="form-control"
|
||||
placeholder="Your name"
|
||||
required
|
||||
/>
|
||||
<textarea
|
||||
id="comment-content"
|
||||
rows="3"
|
||||
class="form-control"
|
||||
placeholder="Add a comment..."
|
||||
required
|
||||
></textarea>
|
||||
<button class="btn-primary" type="submit">Post Comment</button>
|
||||
</form>
|
||||
<div id="comments-container"></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="history-tab">
|
||||
<div id="history-container"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>Ticket System by Honey v1.1 © 2025</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script type="module" src="ticket-user.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,380 @@
|
||||
:root {
|
||||
--color-bg: #2e2e2e;
|
||||
--color-surface: #3c3c3c;
|
||||
--color-primary: #4caf50;
|
||||
--color-primary-hover: #45a049;
|
||||
--color-accent: #2196f3;
|
||||
--color-text: #f5f5f5;
|
||||
--color-muted: #b0b0b0;
|
||||
--color-border: #4f4f4f;
|
||||
--color-danger: #e53935;
|
||||
--color-admin-highlight: #424242;
|
||||
--radius: 8px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
line-height: 1.6;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background-color: var(--color-surface);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 1.8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.back-link {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.back-link i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
}
|
||||
.ticket-details-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
@media (min-width: 900px) {
|
||||
.ticket-details-layout {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
.ticket-details,
|
||||
.ticket-activity {
|
||||
background-color: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ticket-activity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 600px;
|
||||
}
|
||||
.tab-content {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
.tab-content.active {
|
||||
display: flex;
|
||||
}
|
||||
#comments-container,
|
||||
#history-container {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 100%;
|
||||
padding-right: 8px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-primary) transparent;
|
||||
}
|
||||
#comments-container::-webkit-scrollbar,
|
||||
#history-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
#comments-container::-webkit-scrollbar-thumb,
|
||||
#history-container::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ticket-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.ticket-header-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.ticket-header-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.ticket-header h2 {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ticket-status-priority-requester {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
margin-top: 5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ticket-status,
|
||||
.ticket-priority,
|
||||
.ticket-requester,
|
||||
.ticket-category {
|
||||
padding: 6px 14px;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
overflow: visible;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ticket-status {
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
.ticket-priority {
|
||||
background-color: var(--color-accent);
|
||||
color: white;
|
||||
}
|
||||
.ticket-requester,
|
||||
.ticket-category {
|
||||
background-color: var(--color-muted);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
.ticket-meta {
|
||||
margin-top: 10px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-muted);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.ticket-body h3 {
|
||||
margin: 20px 0 10px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
#ticket-description-text {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-right: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@media (min-width: 600px) {
|
||||
.ticket-info-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
}
|
||||
}
|
||||
.info-item h4 {
|
||||
margin-bottom: 4px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.ticket-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr; /* 3 columns */
|
||||
gap: 15px;
|
||||
grid-auto-rows: auto;
|
||||
}
|
||||
.ticket-info-grid .info-item {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.info-item .form-control {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
gap: 10px;
|
||||
}
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.tab-btn:hover {
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
.tab-btn.active {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.comment {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
max-width: 800px;
|
||||
}
|
||||
.comment-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid var(--color-border);
|
||||
}
|
||||
.comment-content {
|
||||
background-color: var(--color-surface);
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius);
|
||||
width: 100%;
|
||||
}
|
||||
.comment-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.comment-author {
|
||||
font-weight: bold;
|
||||
}
|
||||
.comment-date {
|
||||
color: var(--color-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.comment-text {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.comment-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.comment-form input,
|
||||
.comment-form textarea {
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 10px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.comment-form input:focus,
|
||||
.comment-form textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.history-icon {
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.history-content {
|
||||
background-color: var(--color-surface);
|
||||
padding: 10px 15px;
|
||||
border-radius: var(--radius);
|
||||
width: 100%;
|
||||
}
|
||||
.history-action {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.history-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
/* Post Comment Button Styling */
|
||||
.btn-primary {
|
||||
padding: 10px 16px;
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s ease;
|
||||
align-self: flex-start; /* damit er nicht breit wird */
|
||||
}
|
||||
|
||||
.btn-primaryt-btn:hover {
|
||||
background-color: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-muted);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
Reference in new issue
Block a user