first commit

This commit is contained in:
2026-06-29 09:52:02 +02:00
commit da2f7c3c86
31 changed files with 2306 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
"""Excel-Export der Auswertung (mehrere Tabellenblätter)."""
import io
import json
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font, PatternFill
from openpyxl.utils import get_column_letter
from . import analytics, db
HEAD_FILL = PatternFill("solid", fgColor="0B6E4F")
HEAD_FONT = Font(color="FFFFFF", bold=True)
def _style_header(ws, ncols):
for c in range(1, ncols + 1):
cell = ws.cell(row=1, column=c)
cell.fill = HEAD_FILL
cell.font = HEAD_FONT
cell.alignment = Alignment(vertical="center")
ws.freeze_panes = "A2"
def _autosize(ws, max_width=60):
for col in ws.columns:
length = max((len(str(c.value)) for c in col if c.value is not None), default=8)
ws.column_dimensions[get_column_letter(col[0].column)].width = min(length + 2, max_width)
def build_workbook(period_days=None) -> bytes:
m = analytics.compute(period_days)
h = m["humanize"]
wb = Workbook()
# --- KPIs ---
ws = wb.active
ws.title = "KPIs"
ws.append(["Kennzahl", "Wert"])
rows = [
("Tickets gesamt", m["total"]),
("offen", m["open"]),
("geschlossen", m["closed"]),
("eskaliert", m["escalated"]),
("Ø Reaktionszeit", h(m["response"]["avg"])),
("Median Reaktionszeit", h(m["response"]["median"])),
("Ø Lösungszeit", h(m["resolution"]["avg"])),
("Median Lösungszeit", h(m["resolution"]["median"])),
("Ø Alter offener Tickets (Tage)",
round(m["open_age_avg_days"], 1) if m["open_age_avg_days"] else ""),
]
for r in rows:
ws.append(list(r))
_style_header(ws, 2)
_autosize(ws)
# --- Verteilungen ---
ws = wb.create_sheet("Verteilungen")
ws.append(["Kategorie", "Wert", "Anzahl"])
for label, data in (("Status", m["by_state"]), ("Priorität", m["by_priority"]),
("Gruppe", m["by_group"]), ("Bearbeiter", m["by_owner"])):
for key, cnt in data:
ws.append([label, key, cnt])
_style_header(ws, 3)
_autosize(ws)
# --- Monatswerte ---
ws = wb.create_sheet("Monatswerte")
ws.append(["Monat", "erstellt", "geschlossen"])
for row in m["monthly"]:
ws.append([row["month"], row["created"], row["closed"]])
_style_header(ws, 3)
_autosize(ws)
# --- Tickets ---
ws = wb.create_sheet("Tickets")
cols = ["Nummer", "Titel", "Status", "Priorität", "Gruppe", "Bearbeiter",
"Artikel", "erstellt", "1. Reaktion", "geschlossen", "zuletzt geändert"]
ws.append(cols)
with db.get_conn() as conn:
for r in conn.execute("SELECT raw_json FROM tickets ORDER BY zammad_updated_at DESC"):
t = json.loads(r["raw_json"])
ws.append([
t.get("number"), t.get("title"), t.get("state"), t.get("priority"),
t.get("group"), t.get("owner"), t.get("article_count"),
_d(t.get("created_at")), _d(t.get("first_response_at")),
_d(t.get("last_close_at")), _d(t.get("updated_at")),
])
_style_header(ws, len(cols))
_autosize(ws)
# --- Änderungen ---
ws = wb.create_sheet("Änderungen")
ws.append(["Zeitpunkt", "Ticket", "Titel", "Feld", "alt", "neu", "Detail"])
with db.get_conn() as conn:
for c in conn.execute(
"SELECT detected_at, ticket_number, ticket_title, label, old_value, "
"new_value, detail FROM changes ORDER BY detected_at DESC LIMIT 5000"
):
ws.append([_d(c["detected_at"]), c["ticket_number"], c["ticket_title"],
c["label"], c["old_value"], c["new_value"], c["detail"]])
_style_header(ws, 7)
_autosize(ws)
buf = io.BytesIO()
wb.save(buf)
return buf.getvalue()
def _d(value):
if not value:
return ""
return str(value).replace("T", " ")[:16]