first commit
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
{% macro change_row(c) %}
|
||||
<li class="{{ 'unseen' if not c.seen else '' }}"
|
||||
{% if c.detail %}title="{{ c.detail }}"{% endif %}>
|
||||
<span class="tag tag-{{ c.field }}">{{ c.label }}</span>
|
||||
<a class="cnum" href="{{ url_for('ticket_detail', ticket_id=c.ticket_id) }}">#{{ c.ticket_number }}</a>
|
||||
<span class="ctitle">{{ c.ticket_title }}</span>
|
||||
<span class="cchg">{{ c.old_value or '–' }} → <b>{{ c.new_value or '–' }}</b></span>
|
||||
<time>{{ c.detected_at|dt }}</time>
|
||||
{% if not c.seen %}
|
||||
<form method="post" action="{{ url_for('mark_one_read', change_id=c.id) }}" class="mark-one">
|
||||
<button type="submit" title="Als gelesen markieren">✓</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="seen-dot" title="gelesen">·</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endmacro %}
|
||||
@@ -0,0 +1,87 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Auswertung · EPI-Support{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% macro bars(items) %}
|
||||
{% set top = (items|map(attribute=1)|max) if items else 1 %}
|
||||
<div class="barlist">
|
||||
{% for label, cnt in items %}
|
||||
<div class="barrow">
|
||||
<span class="barlabel" title="{{ label }}">{{ label }}</span>
|
||||
<span class="bartrack"><span class="barfill" style="width: {{ (cnt / top * 100) if top else 0 }}%"></span></span>
|
||||
<span class="barval">{{ cnt }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted">Keine Daten.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="chips">
|
||||
{% for val, label in periods %}
|
||||
<a class="chip {{ 'on' if active_period == val }}"
|
||||
href="{{ url_for('analytics_view', period=val) }}">{{ label }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<a class="btn-sm" href="{{ url_for('export_xlsx', period=active_period) }}">⬇ Excel-Export</a>
|
||||
</div>
|
||||
|
||||
<section class="cards cards-6">
|
||||
<div class="card"><span class="num">{{ m.total }}</span><span class="lbl">Tickets im Zeitraum</span></div>
|
||||
<div class="card"><span class="num">{{ m.open }}</span><span class="lbl">offen</span></div>
|
||||
<div class="card"><span class="num">{{ m.closed }}</span><span class="lbl">geschlossen</span></div>
|
||||
<div class="card {{ 'alert' if m.escalated }}"><span class="num">{{ m.escalated }}</span><span class="lbl">eskaliert</span></div>
|
||||
<div class="card"><span class="num">{{ h(m.response.median) }}</span><span class="lbl">Median Reaktionszeit</span></div>
|
||||
<div class="card"><span class="num">{{ h(m.resolution.median) }}</span><span class="lbl">Median Lösungszeit</span></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head"><h2>Ticket-Aufkommen pro Monat</h2></div>
|
||||
<div class="chart-wrap">{{ chart_svg|safe }}</div>
|
||||
</section>
|
||||
|
||||
<div class="grid-2">
|
||||
<section class="panel">
|
||||
<div class="panel-head"><h2>Reaktions- & Lösungszeiten</h2></div>
|
||||
<table class="kv">
|
||||
<tr><th></th><th>Median</th><th>Ø</th><th>P90</th><th>n</th></tr>
|
||||
<tr><td>Reaktionszeit</td>
|
||||
<td>{{ h(m.response.median) }}</td><td>{{ h(m.response.avg) }}</td>
|
||||
<td>{{ h(m.response.p90) }}</td><td>{{ m.response.n }}</td></tr>
|
||||
<tr><td>Lösungszeit</td>
|
||||
<td>{{ h(m.resolution.median) }}</td><td>{{ h(m.resolution.avg) }}</td>
|
||||
<td>{{ h(m.resolution.p90) }}</td><td>{{ m.resolution.n }}</td></tr>
|
||||
</table>
|
||||
{% if m.oldest_open %}
|
||||
<p class="hint">Ältestes offenes Ticket:
|
||||
<a href="{{ url_for('ticket_detail', ticket_id=m.oldest_open.id) }}">#{{ m.oldest_open.number }}</a>
|
||||
{{ m.oldest_open.title }} – seit <b>{{ m.oldest_open.age|round(0)|int }} Tagen</b>
|
||||
{% if m.open_age_avg_days %}(Ø offen: {{ m.open_age_avg_days|round(1) }} Tage){% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head"><h2>Nach Priorität</h2></div>
|
||||
{{ bars(m.by_priority) }}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<section class="panel">
|
||||
<div class="panel-head"><h2>Nach Status</h2></div>
|
||||
{{ bars(m.by_state) }}
|
||||
</section>
|
||||
<section class="panel">
|
||||
<div class="panel-head"><h2>Nach Gruppe</h2></div>
|
||||
{{ bars(m.by_group) }}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head"><h2>Nach Bearbeiter (Agent)</h2></div>
|
||||
{{ bars(m.by_owner) }}
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}EPI-Support{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<meta http-equiv="refresh" content="120">
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<a class="brand" href="{{ url_for('index') }}">EPI Support Monitor</a>
|
||||
<nav>
|
||||
<a href="{{ url_for('index') }}">Übersicht</a>
|
||||
<a href="{{ url_for('analytics_view') }}">Auswertung</a>
|
||||
<a href="{{ url_for('changes') }}">Änderungen</a>
|
||||
<a href="{{ url_for('new_ticket_form') }}">+ Neues Ticket</a>
|
||||
<a href="{{ zammad_url }}" target="_blank" rel="noopener">Zammad ↗</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flash">{{ messages|join(' ') }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
EPI-Support-Monitor · liest <code>{{ zammad_url }}</code> · Seite aktualisiert sich automatisch
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_macros.html" import change_row %}
|
||||
{% block title %}Änderungen · EPI-Support{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Änderungen</h2>
|
||||
<div class="head-tools">
|
||||
<span class="chips">
|
||||
<a class="chip {{ 'on' if show != 'unseen' }}" href="{{ url_for('changes', show='all') }}">alle</a>
|
||||
<a class="chip {{ 'on' if show == 'unseen' }}" href="{{ url_for('changes', show='unseen') }}">nur ungelesene ({{ unseen }})</a>
|
||||
</span>
|
||||
{% if unseen %}
|
||||
<form method="post" action="{{ url_for('mark_read') }}">
|
||||
<button class="btn-sm" type="submit">alle als gelesen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if rows %}
|
||||
<ul class="feed compact">
|
||||
{% for c in rows %}{{ change_row(c) }}{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="muted">{{ 'Keine ungelesenen Änderungen.' if show == 'unseen' else 'Noch keine Änderungen erfasst.' }}</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Bestätigen · EPI-Support{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<section class="panel confirm">
|
||||
<div class="panel-head"><h2>{{ heading }}</h2></div>
|
||||
<div class="confirm-body">
|
||||
<p class="warn">⚠ {{ intro }}</p>
|
||||
<div class="preview-box">{{ preview }}</div>
|
||||
|
||||
<form method="post" action="{{ action_url }}" class="confirm-actions"
|
||||
onsubmit="this.querySelector('button').disabled=true;">
|
||||
{% for name, value in fields.items() %}
|
||||
<input type="hidden" name="{{ name }}" value="{{ value }}">
|
||||
{% endfor %}
|
||||
<a class="btn-ghost" href="{{ cancel_url }}">Abbrechen</a>
|
||||
<button class="btn-danger" type="submit">Verbindlich an EPI senden</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,103 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_macros.html" import change_row %}
|
||||
{% block title %}Übersicht · EPI-Support{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<section class="cards">
|
||||
<div class="card"><span class="num">{{ stats.total }}</span><span class="lbl">Tickets gesamt</span></div>
|
||||
<div class="card"><span class="num">{{ stats.open }}</span><span class="lbl">offen</span></div>
|
||||
<div class="card {{ 'alert' if stats.unseen else '' }}">
|
||||
<span class="num">{{ stats.unseen }}</span><span class="lbl">ungelesene Änderungen</span>
|
||||
</div>
|
||||
<div class="card status">
|
||||
{% if last_run %}
|
||||
<span class="num {{ 'ok' if last_run.status == 'ok' else 'err' }}">
|
||||
{{ '✓' if last_run.status == 'ok' else '!' }}
|
||||
</span>
|
||||
<span class="lbl">letzter Abruf<br>{{ last_run.finished_at|dt }}</span>
|
||||
{% else %}
|
||||
<span class="num">…</span><span class="lbl">startet</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid">
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Ungelesene Änderungen{% if stats.unseen %} ({{ stats.unseen }}){% endif %}</h2>
|
||||
{% if stats.unseen %}
|
||||
<form method="post" action="{{ url_for('mark_read') }}">
|
||||
<button class="btn-sm" type="submit">alle als gelesen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if recent %}
|
||||
<ul class="feed compact">
|
||||
{% for c in recent %}{{ change_row(c) }}{% endfor %}
|
||||
</ul>
|
||||
{% if stats.unseen > recent|length %}
|
||||
<a class="more" href="{{ url_for('changes', show='unseen') }}">
|
||||
+ {{ stats.unseen - recent|length }} weitere ungelesene anzeigen →</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="muted">Alles gelesen – keine offenen Änderungen. 🎉</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head"><h2>Status-Filter</h2></div>
|
||||
<div class="chips">
|
||||
<a class="chip {{ 'on' if not active_state }}" href="{{ url_for('index', sort=sort, dir=dir) }}">alle</a>
|
||||
<a class="chip {{ 'on' if active_state == 'not_closed' }}"
|
||||
href="{{ url_for('index', state='not_closed', sort=sort, dir=dir) }}">nicht geschlossen</a>
|
||||
{% for s in states %}
|
||||
<a class="chip {{ 'on' if active_state == s.state }}"
|
||||
href="{{ url_for('index', state=s.state, sort=sort, dir=dir) }}">{{ s.state }} ({{ s.c }})</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{% macro th(col, label) %}
|
||||
{% set nextdir = 'asc' if (sort == col and dir == 'desc') else 'desc' %}
|
||||
<th class="{{ 'sorted' if sort == col }}">
|
||||
<a href="{{ url_for('index', state=active_state or none, sort=col, dir=nextdir) }}">
|
||||
{{ label }}{% if sort == col %} <span class="arrow">{{ '▼' if dir == 'desc' else '▲' }}</span>{% endif %}
|
||||
</a>
|
||||
</th>
|
||||
{% endmacro %}
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Tickets{% if active_state == 'not_closed' %} – nicht geschlossen{% elif active_state %} – {{ active_state }}{% endif %}</h2>
|
||||
</div>
|
||||
<table class="tickets">
|
||||
<thead>
|
||||
<tr>
|
||||
{{ th('number', '#') }}{{ th('title', 'Titel') }}{{ th('state', 'Status') }}
|
||||
{{ th('priority', 'Priorität') }}{{ th('owner', 'Bearbeiter') }}
|
||||
{{ th('articles', 'Artikel') }}{{ th('updated', 'zuletzt geändert') }}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in tickets %}
|
||||
{% set n = unseen_map.get(t.id) %}
|
||||
<tr class="{{ 'row-unseen' if n }}">
|
||||
<td>
|
||||
{% if n %}<span class="badge" title="{{ n }} unbestätigte Änderung(en)">{{ n }}</span>{% endif %}
|
||||
<a href="{{ url_for('ticket_detail', ticket_id=t.id) }}">{{ t.number }}</a>
|
||||
</td>
|
||||
<td class="title">{{ t.title }}</td>
|
||||
<td><span class="state state-{{ t.state }}">{{ t.state }}</span></td>
|
||||
<td>{{ t.priority }}</td>
|
||||
<td>{{ t.owner if t.owner and t.owner != '-' else '—' }}</td>
|
||||
<td>{{ t.article_count }}</td>
|
||||
<td><time>{{ t.zammad_updated_at|dt }}</time></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="7" class="muted">Keine Tickets.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Neues Ticket · EPI-Support{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<p><a class="back" href="{{ url_for('index') }}">← zurück zur Übersicht</a></p>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head"><h2>Neues Ticket beim EPI-Support anlegen</h2></div>
|
||||
<form method="post" action="{{ url_for('new_ticket_preview') }}" class="form">
|
||||
<label>Titel / Betreff
|
||||
<input type="text" name="title" required maxlength="200"
|
||||
placeholder="Kurze Zusammenfassung des Anliegens">
|
||||
</label>
|
||||
<label>Gruppe
|
||||
<select name="group" required>
|
||||
{% for g in groups %}
|
||||
<option value="{{ g }}" {{ 'selected' if g == 'EPI::Support' }}>{{ g }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>Nachricht
|
||||
<textarea name="body" rows="10" required
|
||||
placeholder="Beschreibe dein Anliegen…"></textarea>
|
||||
</label>
|
||||
<div class="form-actions">
|
||||
<button class="btn-sm" type="submit">Weiter zur Bestätigung →</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,86 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}#{{ ticket.number }} · EPI-Support{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<p><a class="back" href="{{ url_for('index') }}">← zurück zur Übersicht</a></p>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>#{{ ticket.number }} – {{ ticket.title }}</h2>
|
||||
<a class="btn-sm" href="{{ zammad_url }}/#ticket/zoom/{{ ticket.id }}"
|
||||
target="_blank" rel="noopener">in Zammad öffnen ↗</a>
|
||||
</div>
|
||||
<dl class="meta">
|
||||
<div><dt>Status</dt><dd><span class="state state-{{ ticket.state }}">{{ ticket.state }}</span></dd></div>
|
||||
<div><dt>Priorität</dt><dd>{{ ticket.priority }}</dd></div>
|
||||
<div><dt>Bearbeiter</dt><dd>{{ ticket.owner if ticket.owner and ticket.owner != '-' else '—' }}</dd></div>
|
||||
<div><dt>Gruppe</dt><dd>{{ ticket.group }}</dd></div>
|
||||
<div><dt>Artikel</dt><dd>{{ ticket.article_count }}</dd></div>
|
||||
<div><dt>erstellt</dt><dd>{{ ticket.zammad_created_at|dt }}</dd></div>
|
||||
<div><dt>zuletzt geändert</dt><dd>{{ ticket.zammad_updated_at|dt }}</dd></div>
|
||||
<div><dt>geschlossen</dt><dd>{{ ticket.last_close_at|dt }}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Konversation{% if conversation %} ({{ conversation|length }}){% endif %}</h2>
|
||||
</div>
|
||||
{% if conv_error %}
|
||||
<p class="muted">Konversation konnte nicht geladen werden: {{ conv_error }}</p>
|
||||
{% elif conversation %}
|
||||
<div class="thread">
|
||||
{% for a in conversation %}
|
||||
<article class="msg msg-{{ a.sender|lower if a.sender else 'system' }}">
|
||||
<header class="msg-head">
|
||||
<span class="who">
|
||||
<span class="role role-{{ a.sender|lower if a.sender else 'system' }}">{{ a.sender or 'System' }}</span>
|
||||
{{ a.frm }}
|
||||
</span>
|
||||
<time>{{ a.created_at|dt }}</time>
|
||||
</header>
|
||||
{% if a.subject %}<div class="msg-subj">{{ a.subject }}</div>{% endif %}
|
||||
<div class="msg-body">{{ a.body }}</div>
|
||||
{% if a.attachments %}
|
||||
<div class="msg-att">📎 {{ a.attachments|join(', ') }}</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted">Keine Nachrichten vorhanden.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if ticket.state != 'closed' %}
|
||||
<form method="post" action="{{ url_for('reply_preview', ticket_id=ticket.id) }}" class="reply-form">
|
||||
<textarea name="body" rows="4" required
|
||||
placeholder="Antwort an den EPI-Support verfassen…"></textarea>
|
||||
<div class="form-actions">
|
||||
<span class="hint-inline">Geht nach Bestätigung live an EPI.</span>
|
||||
<button class="btn-sm" type="submit">Antworten →</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="muted">Ticket ist geschlossen – zum Antworten ggf. ein neues Ticket anlegen.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head"><h2>Verlauf (von uns erfasst)</h2></div>
|
||||
{% if timeline %}
|
||||
<ul class="timeline">
|
||||
{% for c in timeline %}
|
||||
<li>
|
||||
<span class="tag tag-{{ c.field }}">{{ c.label }}</span>
|
||||
<div class="chg">{{ c.old_value or '–' }} → <b>{{ c.new_value or '–' }}</b>
|
||||
{% if c.detail %}<div class="detail">{{ c.detail }}</div>{% endif %}
|
||||
</div>
|
||||
<time>{{ c.detected_at|dt }}</time>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="muted">Seit Beobachtungsbeginn keine Änderung erfasst.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user