OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

my-tickets.php

22 KB
<?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">&#128100; 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">&#128065; 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, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;');
    }

    // ----------------------------------------------------------------
    // 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>