Quelltext des aktuellen Standes
<?php if (!defined('ABSPATH')) exit;
$status_labels = [
'new' => ['label' => 'Neu', 'class' => 'opennit-status-new'],
'open' => ['label' => 'Offen', 'class' => 'opennit-status-open'],
'waiting' => ['label' => 'Wartend', 'class' => 'opennit-status-waiting'],
'solved' => ['label' => 'Gelöst', 'class' => 'opennit-status-solved'],
'closed' => ['label' => 'Geschlossen', 'class' => 'opennit-status-closed'],
];
?>
<div class="opennit-wrap" id="opennit-my-tickets-wrap">
<div class="opennit-card opennit-tickets-card">
<!-- Kopfbereich -->
<div class="opennit-card__head">
<div class="opennit-card__head-icon" aria-hidden="true">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 8a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2 2 2 0 0 0 0 4 2 2 0 0 1-2 2H5a2 2 0 0 1-2-2 2 2 0 0 0 0-4Z"/>
<path d="M8 11h8"/>
</svg>
</div>
<div class="opennit-card__head-text">
<h2 class="opennit-card__title"><?= esc_html($text_my_title) ?></h2>
<p class="opennit-card__subtitle">Ihre Anfragen an das IT-Team im Überblick.</p>
</div>
</div>
<?php if (empty($tickets)): ?>
<div class="opennit-card__body">
<div class="opennit-empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>
<p><?= esc_html($text_no_tickets) ?></p>
</div>
</div>
<?php else: ?>
<div class="opennit-tickets-toolbar">
<div class="opennit-ticket-filter-tabs" id="opennit-ticket-tabs">
<button class="opennit-tab opennit-tab-active" data-filter="all">
Alle <span class="opennit-tab-count" id="opennit-tab-count-all"><?= count($tickets) ?></span>
</button>
<button class="opennit-tab" data-filter="open">
Offen <span class="opennit-tab-count" id="opennit-tab-count-open"></span>
</button>
<button class="opennit-tab" data-filter="closed">
Abgeschlossen <span class="opennit-tab-count" id="opennit-tab-count-closed"></span>
</button>
</div>
<p class="opennit-tickets-limit-hint">Letzte <?= count($tickets) ?> Tickets</p>
</div>
<div class="opennit-tickets-list" id="opennit-ticket-list">
<?php foreach ($tickets as $ticket):
$status = $ticket['status'] ?? 'new';
$st = $status_labels[$status] ?? ['label' => $status, 'class' => ''];
$cat = $ticket['category_name'] ?: '–';
$loc = $ticket['location_name'] ?: '–';
$replies = (int)($ticket['reply_count'] ?? 0);
$tid = (int)$ticket['id'];
?>
<div class="opennit-ticket-row" data-status="<?= esc_attr($status) ?>" data-ticket-id="<?= $tid ?>">
<!-- Klickbarer Header -->
<div class="opennit-ticket-row-header"
onclick="opennitToggleTicket(<?= $tid ?>, this)"
aria-expanded="false" role="button" tabindex="0">
<div class="opennit-ticket-row-main">
<div class="opennit-ticket-row-top">
<span class="opennit-ticket-id">#<?= $tid ?></span>
<span class="opennit-ticket-subject"><?= esc_html($ticket['subject']) ?></span>
<span class="opennit-ticket-status <?= esc_attr($st['class']) ?>">
<span class="opennit-status-dot"></span><?= esc_html($st['label']) ?>
</span>
</div>
<div class="opennit-ticket-card-meta">
<span class="opennit-meta-item">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
<?= esc_html($ticket['created_at']) ?>
</span>
<span class="opennit-meta-item">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.59 13.41 13.42 20.59a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82Z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>
<?= esc_html($cat) ?>
</span>
<span class="opennit-meta-item">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13S3 17 3 10a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
<?= esc_html($loc) ?>
</span>
<?php if ($replies > 0): ?>
<span class="opennit-meta-item opennit-meta-replies">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
<?= $replies ?> <?= $replies === 1 ? 'Antwort' : 'Antworten' ?>
</span>
<?php endif; ?>
</div>
</div>
<div class="opennit-ticket-chevron">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
</div>
</div>
<!-- Expandierbarer Detail-Bereich (lazy per AJAX) -->
<div class="opennit-ticket-detail" id="opennit-detail-<?= $tid ?>">
<div class="opennit-ticket-detail-loading">Wird geladen…</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<script>
(function() {
var ajaxUrl = '<?= esc_js(admin_url('admin-ajax.php')) ?>';
var nonce = '<?= esc_js($nonce ?? wp_create_nonce('opennit_nonce')) ?>';
var loaded = {};
window.opennitToggleTicket = function(ticketId, headerEl) {
var row = headerEl.closest ? headerEl.closest('.opennit-ticket-row') : headerEl.parentElement;
var detail = document.getElementById('opennit-detail-' + ticketId);
var chevron = row.querySelector('.opennit-ticket-chevron');
var isOpen = detail.classList.contains('is-open');
if (isOpen) {
detail.classList.remove('is-open');
headerEl.setAttribute('aria-expanded', 'false');
if (chevron) chevron.style.transform = '';
return;
}
detail.classList.add('is-open');
headerEl.setAttribute('aria-expanded', 'true');
if (chevron) chevron.style.transform = 'rotate(180deg)';
if (loaded[ticketId]) return;
var fd = new FormData();
fd.append('action', 'opennit_get_ticket');
fd.append('nonce', nonce);
fd.append('ticket_id', ticketId);
fetch(ajaxUrl, { method: 'POST', body: fd })
.then(function(r) { return r.json(); })
.then(function(res) {
if (!res.success) {
detail.innerHTML = '<div class="opennit-notice opennit-notice-error" style="margin:0.75rem;">'
+ escHtml(res.data || 'Fehler beim Laden.') + '</div>';
return;
}
loaded[ticketId] = true;
detail.innerHTML = renderDetail(ticketId, res.data);
})
.catch(function() {
detail.innerHTML = '<div class="opennit-notice opennit-notice-error" style="margin:0.75rem;">Netzwerkfehler.</div>';
});
};
// Ersetzt <img src="/uploads/..."> durch den WP-Proxy (Token bleibt server-seitig)
function rewriteInlineImages(html) {
return html.replace(/src="\/uploads\/([^"]+)"/gi, function(match, imgPath) {
return 'src="' + ajaxUrl + '?action=opennit_proxy_image&path=' + encodeURIComponent(imgPath) + '&nonce=' + nonce + '"';
});
}
function renderDetail(ticketId, data) {
var html = '';
// 1) Ansprechpartner + Beobachter
var metaParts = [];
if (data.assignees && data.assignees.length > 0) {
var names = data.assignees.map(function(a) { return escHtml(a.display_name || a.email); }).join(', ');
metaParts.push('<span class="opennit-ticket-detail-meta-item">'
+ '<span class="opennit-ticket-detail-meta-label">👤 Ansprechpartner:</span> ' + names + '</span>');
}
if (data.watchers && data.watchers.length > 0) {
var wnames = data.watchers.map(function(w) { return escHtml(w.name || w.email); }).join(', ');
metaParts.push('<span class="opennit-ticket-detail-meta-item">'
+ '<span class="opennit-ticket-detail-meta-label">👁 Beobachter:</span> ' + wnames + '</span>');
}
if (metaParts.length > 0) {
html += '<div class="opennit-ticket-detail-meta">' + metaParts.join('') + '</div>';
}
// 2) Verknüpftes Asset
if (data.asset) {
var al = [data.asset.manufacturer, data.asset.type,
data.asset.inventory_number ? '(' + data.asset.inventory_number + ')' : '']
.filter(Boolean).join(' ');
html += '<div class="opennit-ticket-asset-link">'
+ '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">'
+ '<rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-4 0v2"/></svg> '
+ escHtml(al) + '</div>';
}
// 3) Anhänge ohne Reply-Zuordnung (Ticket-Ebene)
var topAtts = (data.attachments || []).filter(function(a) { return !a.reply_id; });
if (topAtts.length > 0) {
topAtts.forEach(function(att) {
var url = ajaxUrl + '?action=opennit_download_attachment&id=' + att.id + '&nonce=' + nonce;
html += '<a href="' + url + '" class="opennit-attachment-link" target="_blank">'
+ '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">'
+ '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>'
+ '<polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> '
+ escHtml(att.original_name) + '</a> ';
});
}
// 4) Nachrichten-Thread
html += '<div class="opennit-reply-thread">';
// Ursprüngliche Ticket-Nachricht zuerst
if (data.ticket && data.ticket.body) {
var senderName = escHtml(data.ticket.from_name || data.ticket.from_email || 'Einsender');
html += '<div class="opennit-reply opennit-reply-initial">'
+ '<div class="opennit-reply-header">'
+ '<span style="display:inline-flex;align-items:center;gap:0.4rem;">'
+ '<span class="opennit-reply-role-badge opennit-role-initial">Einsender</span>'
+ '<span class="opennit-reply-author">' + senderName + '</span>'
+ '</span>'
+ '<span class="opennit-reply-date">' + escHtml(data.ticket.created_at) + '</span>'
+ '</div>'
+ '<div class="opennit-reply-body">' + data.ticket.body + '</div>'
+ '</div>';
}
// Antworten
if (data.replies && data.replies.length > 0) {
data.replies.forEach(function(r) {
var isStaff = r.author_type === 'staff';
var cls = isStaff ? 'opennit-reply-staff' : 'opennit-reply-customer';
var badgeCls = isStaff ? 'opennit-role-staff' : 'opennit-role-customer';
var badgeTxt = isStaff ? 'IT-Team' : 'Einsender';
var authorTxt = escHtml(r.author_name || (isStaff ? 'IT-Team' : 'Sie'));
// Inline-Bilder im Body über WP-Proxy ausliefern
var body = rewriteInlineImages(r.body || '');
// Anhänge dieser Antwort
var replyAtts = (data.attachments || []).filter(function(a) {
return a.reply_id && a.reply_id === r.id;
});
var attsHtml = '';
if (replyAtts.length > 0) {
attsHtml += '<div class="opennit-reply-attachments">';
replyAtts.forEach(function(att) {
var url = ajaxUrl + '?action=opennit_download_attachment&id=' + att.id + '&nonce=' + nonce;
if (att.mime_type && att.mime_type.indexOf('image/') === 0) {
attsHtml += '<img src="' + url + '" alt="' + escHtml(att.original_name) + '" class="opennit-reply-image">';
} else {
attsHtml += '<a href="' + url + '" class="opennit-attachment-link" target="_blank">'
+ '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">'
+ '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>'
+ '<polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> '
+ escHtml(att.original_name) + '</a> ';
}
});
attsHtml += '</div>';
}
html += '<div class="opennit-reply ' + cls + '">'
+ '<div class="opennit-reply-header">'
+ '<span style="display:inline-flex;align-items:center;gap:0.4rem;">'
+ '<span class="opennit-reply-role-badge ' + badgeCls + '">' + badgeTxt + '</span>'
+ '<span class="opennit-reply-author">' + authorTxt + '</span>'
+ '</span>'
+ '<span class="opennit-reply-date">' + escHtml(r.created_at) + '</span>'
+ '</div>'
+ '<div class="opennit-reply-body">' + body + '</div>'
+ attsHtml
+ '</div>';
});
}
html += '</div>'; // .opennit-reply-thread
// 5) Antwort-Formular (nicht bei geschlossenen Tickets)
if (data.ticket && data.ticket.status === 'closed') {
html += '<div class="opennit-ticket-closed-notice">Dieses Ticket ist geschlossen. Eine Antwort ist nicht mehr möglich.</div>';
} else {
html += '<div class="opennit-reply-form" id="opennit-reply-form-' + ticketId + '">'
+ '<div class="opennit-reply-form-title">Antworten</div>'
+ '<textarea class="opennit-reply-textarea" id="opennit-reply-msg-' + ticketId
+ '" placeholder="Ihre Antwort…" rows="3"></textarea><br>'
+ '<button type="button" class="opennit-reply-send-btn" onclick="opennitSendReply(' + ticketId + ')">'
+ '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">'
+ '<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg> Senden'
+ '</button>'
+ '<div class="opennit-reply-feedback" id="opennit-reply-fb-' + ticketId + '"></div>'
+ '</div>';
}
return html;
}
window.opennitSendReply = function(ticketId) {
var textarea = document.getElementById('opennit-reply-msg-' + ticketId);
var fb = document.getElementById('opennit-reply-fb-' + ticketId);
var btn = fb.previousElementSibling;
var msg = textarea ? textarea.value.trim() : '';
if (!msg) return;
btn.disabled = true;
fb.style.display = 'none';
var fd = new FormData();
fd.append('action', 'opennit_reply_ticket');
fd.append('nonce', nonce);
fd.append('ticket_id', ticketId);
fd.append('message', msg);
fetch(ajaxUrl, { method: 'POST', body: fd })
.then(function(r) { return r.json(); })
.then(function(res) {
if (!res.success) {
fb.textContent = res.data || 'Fehler beim Senden.';
fb.className = 'opennit-reply-feedback opennit-feedback-error';
fb.style.display = 'block';
btn.disabled = false;
return;
}
textarea.value = '';
var thread = document.querySelector('#opennit-detail-' + ticketId + ' .opennit-reply-thread');
var reply = res.data.reply;
if (reply) {
var senderName = escHtml(reply.author_name || 'Sie');
var div = document.createElement('div');
div.className = 'opennit-reply opennit-reply-customer';
div.innerHTML = '<div class="opennit-reply-header">'
+ '<span style="display:inline-flex;align-items:center;gap:0.4rem;">'
+ '<span class="opennit-reply-role-badge opennit-role-customer">Einsender</span>'
+ '<span class="opennit-reply-author">' + senderName + '</span>'
+ '</span>'
+ '<span class="opennit-reply-date">' + escHtml(reply.created_at) + '</span>'
+ '</div>'
+ '<div class="opennit-reply-body">' + reply.body + '</div>';
if (thread) {
thread.appendChild(div);
} else {
var form = document.getElementById('opennit-reply-form-' + ticketId);
var newThread = document.createElement('div');
newThread.className = 'opennit-reply-thread';
newThread.appendChild(div);
form.parentNode.insertBefore(newThread, form);
}
}
fb.textContent = 'Antwort gesendet.';
fb.className = 'opennit-reply-feedback opennit-feedback-success';
fb.style.display = 'block';
btn.disabled = false;
delete loaded[ticketId];
})
.catch(function() {
fb.textContent = 'Netzwerkfehler.';
fb.className = 'opennit-reply-feedback opennit-feedback-error';
fb.style.display = 'block';
btn.disabled = false;
});
};
function escHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
// ----------------------------------------------------------------
// Ticket-Filter-Tabs
// ----------------------------------------------------------------
var openStatuses = ['new', 'open', 'waiting'];
var closedStatuses = ['solved', 'closed'];
function opennitUpdateTabCounts() {
var rows = document.querySelectorAll('#opennit-ticket-list .opennit-ticket-row');
var countOpen = 0, countClosed = 0;
rows.forEach(function(r) {
var s = r.getAttribute('data-status') || '';
if (openStatuses.indexOf(s) !== -1) countOpen++;
if (closedStatuses.indexOf(s) !== -1) countClosed++;
});
var elOpen = document.getElementById('opennit-tab-count-open');
var elClosed = document.getElementById('opennit-tab-count-closed');
if (elOpen) elOpen.textContent = countOpen || '';
if (elClosed) elClosed.textContent = countClosed || '';
}
function opennitFilterTickets(filter) {
var rows = document.querySelectorAll('#opennit-ticket-list .opennit-ticket-row');
rows.forEach(function(r) {
var s = r.getAttribute('data-status') || '';
var show = filter === 'all'
|| (filter === 'open' && openStatuses.indexOf(s) !== -1)
|| (filter === 'closed' && closedStatuses.indexOf(s) !== -1);
r.classList.toggle('opennit-ticket-hidden', !show);
});
}
var tabsEl = document.getElementById('opennit-ticket-tabs');
if (tabsEl) {
opennitUpdateTabCounts();
tabsEl.addEventListener('click', function(e) {
var btn = e.target.closest('.opennit-tab');
if (!btn) return;
tabsEl.querySelectorAll('.opennit-tab').forEach(function(t) {
t.classList.remove('opennit-tab-active');
});
btn.classList.add('opennit-tab-active');
opennitFilterTickets(btn.getAttribute('data-filter'));
});
}
})();
</script>