From 784b660402c9f9d7918e6c36908f1a0d0f0acb33 Mon Sep 17 00:00:00 2001 From: Honey Date: Sat, 15 Nov 2025 13:48:56 +0100 Subject: [PATCH] Dateien nach "public/ticketview" hochladen --- public/ticketview/admin-view.css | 122 ++++++++ public/ticketview/ticket-admin.js | 311 +++++++++++++++++++ public/ticketview/ticket-view-admin.html | 153 +++++++++ public/ticketview/ticket-view-user.html | 119 +++++++ public/ticketview/ticket-view.css | 380 +++++++++++++++++++++++ 5 files changed, 1085 insertions(+) create mode 100644 public/ticketview/admin-view.css create mode 100644 public/ticketview/ticket-admin.js create mode 100644 public/ticketview/ticket-view-admin.html create mode 100644 public/ticketview/ticket-view-user.html create mode 100644 public/ticketview/ticket-view.css diff --git a/public/ticketview/admin-view.css b/public/ticketview/admin-view.css new file mode 100644 index 0000000..444fd1b --- /dev/null +++ b/public/ticketview/admin-view.css @@ -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; + } +} diff --git a/public/ticketview/ticket-admin.js b/public/ticketview/ticket-admin.js new file mode 100644 index 0000000..c37d097 --- /dev/null +++ b/public/ticketview/ticket-admin.js @@ -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 ` +
+ ${isAdmin ? 'Admin' : 'User'} Avatar +
+
+ ${comment.author} + ${isAdmin ? 'Admin' : ''} + ${formatDateTime(comment.date)} +
+
${comment.content}
+
+
`; + }).join(''); + } + + async function loadHistory() { + const history = await fetchHistory(ticketId); + const container = document.getElementById('history-container'); + container.innerHTML = history.map(h => ` +
+
+
+
${h.action}
+
${h.message || ''}
+
+ By ${h.author || h.actor || 'unknown'} + ${formatDateTime(h.date || h.timestamp)} +
+
+
`).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(); +}); \ No newline at end of file diff --git a/public/ticketview/ticket-view-admin.html b/public/ticketview/ticket-view-admin.html new file mode 100644 index 0000000..5f75152 --- /dev/null +++ b/public/ticketview/ticket-view-admin.html @@ -0,0 +1,153 @@ + + + + + + Ticket Details (Admin) - Ticket System + + + + + + +
+
+

Ticket Details (Admin)

+ +
+ +
+
+ +
+
+
+

Loading...

+ +
+
+ #- +
+
+ +
+
+

Requester

+

-

+
+ +
+

Description

+

Loading...

+ +
+ +
+

Created

+

-

+
+ +
+

Last Updated

+

-

+
+ +
+

Time

+

0

+
+ +
+

+ +
+ +
+

+ +
+ +
+

+ +
+
+ +
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ + + +
+ +
+ + + +
+
+ + +
+ + +
+
+ +
+
+
+
+
+
+ +
+

Ticket System by Honey v1.1 © 2025

+
+
+ + + + \ No newline at end of file diff --git a/public/ticketview/ticket-view-user.html b/public/ticketview/ticket-view-user.html new file mode 100644 index 0000000..3a7ec7f --- /dev/null +++ b/public/ticketview/ticket-view-user.html @@ -0,0 +1,119 @@ + + + + + + My Ticket - Ticket System + + + + + +
+
+

My Ticket

+ +
+ +
+
+
+
+

Loading...

+
+ #- +
+
+ +
+ +
+

Requester

+

-

+
+ + +

Description

+

Loading...

+ + +
+
+

Created

+

-

+
+
+

Last Updated

+

-

+
+
+

Time

+

0

+
+
+

Status

+ Open +
+
+

Priority

+ - +
+
+

Category

+ - +
+
+
+
+ +
+
+ + +
+ +
+
+ + + +
+
+
+ +
+
+
+
+
+
+ +
+

Ticket System by Honey v1.1 © 2025

+
+
+ + + + diff --git a/public/ticketview/ticket-view.css b/public/ticketview/ticket-view.css new file mode 100644 index 0000000..079b7cd --- /dev/null +++ b/public/ticketview/ticket-view.css @@ -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); +} \ No newline at end of file