diff --git a/RELEASES.json b/RELEASES.json index f505a29..338b62a 100644 --- a/RELEASES.json +++ b/RELEASES.json @@ -1,4 +1,15 @@ [ + { + "version": "2026-07-24T20:37Z", + "date": "2026-07-24", + "title": "Neuer Verbrauchsrechner im Verwaltungsbereich", + "items": [ + "Neuer Rechner-Tab zeigt Kosten übersichtlich nach Verbrauchsposten (Credits-Basierte Abrechnung)", + "Übersichtskarten mit aktuellen Credit-Sätzen direkt über dem Rechner", + "Credit-Sätze per Regler individuell anpassbar – mit Empfehlung aus den letzten 30 Tagen", + "Änderungen an den Reglern werden sofort in den Beschriftungen sichtbar" + ] + }, { "version": "2026-05-22T12:41Z", "date": "2026-05-22", diff --git a/src/main.py b/src/main.py index 84674c3..e780557 100644 --- a/src/main.py +++ b/src/main.py @@ -11,7 +11,7 @@ from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from config import STATIC_DIR, PORT -from routers import auth, organizations, licenses, users, dashboard, sources, token_usage, audit, translation, x_scraper +from routers import auth, organizations, licenses, users, dashboard, sources, token_usage, audit, translation, x_scraper, pricing logging.basicConfig( level=logging.INFO, @@ -44,6 +44,7 @@ app.include_router(token_usage.router) app.include_router(audit.router) app.include_router(translation.router) app.include_router(x_scraper.router) +app.include_router(pricing.router) # --- Statische Dateien --- app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") diff --git a/src/routers/pricing.py b/src/routers/pricing.py new file mode 100644 index 0000000..5b10a96 --- /dev/null +++ b/src/routers/pricing.py @@ -0,0 +1,66 @@ +"""Preis-Empfehlung aus den echten API-Kosten der letzten Tage. + +Reine Leseabfrage. Die Empfehlung ist eine betreiberweite Kennzahl (Median der +tatsaechlichen Refresh-Kosten je Lagentyp), deshalb wird die geteilte Live-DB +read-only gelesen, unabhaengig vom DB_PATH des jeweiligen Portals. +""" +import logging +import math +import os +import statistics +from collections import defaultdict +from datetime import datetime, timedelta + +import aiosqlite +from fastapi import APIRouter, Depends +from auth import get_current_admin + +logger = logging.getLogger("verwaltung.pricing") +router = APIRouter(prefix="/api/pricing", tags=["Pricing"]) + +COST_BASIS_DB = os.environ.get("COST_BASIS_DB_PATH", "/home/claude-dev/osint-data/osint.db") +USD_PER_CREDIT = 0.20 + + +@router.get("/cost-basis") +async def cost_basis(days: int = 30, admin=Depends(get_current_admin)): + """Median der echten Refresh-Kosten je Lagentyp und daraus abgeleitete + Credit-Empfehlung (Median USD geteilt durch 0,20 USD, aufgerundet).""" + days = max(1, min(365, days)) + cutoff = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") + result = {"days": days, "usd_per_credit": USD_PER_CREDIT, "typen": {}} + + try: + db = await aiosqlite.connect(f"file:{COST_BASIS_DB}?mode=ro", uri=True) + try: + cursor = await db.execute( + "SELECT i.type, r.total_cost_usd " + "FROM refresh_log r JOIN incidents i ON i.id = r.incident_id " + "WHERE r.status = 'completed' AND r.started_at >= ?", + (cutoff,), + ) + rows = await cursor.fetchall() + finally: + await db.close() + except Exception as e: + logger.warning("cost-basis DB-Fehler: %s", e) + result["fehler"] = "Kostendaten nicht verfuegbar" + return result + + gruppen = defaultdict(list) + for typ, kosten in rows: + gruppen[typ or "adhoc"].append(kosten or 0.0) + + for db_key, label in (("adhoc", "live"), ("research", "research")): + werte = gruppen.get(db_key, []) + if werte: + median = statistics.median(werte) + result["typen"][label] = { + "median_usd": round(median, 2), + "n": len(werte), + "empfohlen": int(math.ceil(median / USD_PER_CREDIT)) if median > 0 else 0, + } + else: + result["typen"][label] = {"median_usd": None, "n": 0, "empfohlen": None} + + return result diff --git a/src/static/js/app.js b/src/static/js/app.js index 2558b0b..cf47dab 100644 --- a/src/static/js/app.js +++ b/src/static/js/app.js @@ -1047,3 +1047,10 @@ document.addEventListener("DOMContentLoaded", () => { loadMeta(); } }); + +// --- Verbrauchsrechner nachladen (eigenstaendiges Modul, siehe rechner.js) --- +(function () { + var s = document.createElement("script"); + s.src = "/static/js/rechner.js?v=20260724d"; + document.head.appendChild(s); +})(); diff --git a/src/static/js/rechner.js b/src/static/js/rechner.js new file mode 100644 index 0000000..24fd0ab --- /dev/null +++ b/src/static/js/rechner.js @@ -0,0 +1,488 @@ +"use strict"; + +// Verbrauchsrechner (Posten-Modell) fuer das Verwaltungsportal. +// Eigenstaendiges Modul, injiziert Tab, Bereich und Styles selbst, +// damit dashboard.html unveraendert bleiben kann. +// Saetze sind ueber Regler frei einstellbar, mit Empfehlung aus den echten +// API-Kosten der letzten 30 Tage (GET /api/pricing/cost-basis). + +(function () { + var USD = 0.20; + var MINIMUM_TAKT_MINUTEN = 30; + + // Frei einstellbare Saetze (Credits je Aktion). + var saetze = { live: 45, research: 40, neben: 1 }; + function rechercheNeu() { return saetze.research * 3; } + + var EINHEITEN = [ + { wert: 1, text: "Minuten" }, + { wert: 60, text: "Stunden" }, + { wert: 1440, text: "Tage" } + ]; + + var BLOCK_NAMEN = { live: "Live-Monitoring", recherche: "Recherche", neben: "Nebenkosten" }; + var BLOCK_REIHENFOLGE = ["live", "recherche", "neben"]; + + var bloecke = { + live: [{ anzahl: 1, nurManuell: false, taktWert: 1, taktEinheit: 1440, tage: 30, manuell: 0 }], + recherche: [{ anzahl: 1, vertiefungen: 1 }], + neben: [{ auto: true, aufrufe: 50 }] + }; + + function fmt(n) { return Math.round(n).toLocaleString("de-DE"); } + function fmtUsd(n) { return n.toLocaleString("de-DE", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + " $"; } + + function neuerPosten(typ) { + if (typ === "recherche") return { anzahl: 1, vertiefungen: 1 }; + if (typ === "neben") return { auto: true, aufrufe: 50 }; + return { anzahl: 1, nurManuell: false, taktWert: 1, taktEinheit: 1440, tage: 30, manuell: 0 }; + } + + function autoAufrufe() { + var summe = 0; + bloecke.live.forEach(function (p) { summe += p.anzahl; }); + bloecke.recherche.forEach(function (p) { summe += p.anzahl; }); + return Math.round(summe * 1.5); + } + + function postenCredits(typ, p) { + if (typ === "live") { + var laeufe = 1; + if (!p.nurManuell && p.taktWert > 0) { + var intervallMin = Math.max(MINIMUM_TAKT_MINUTEN, p.taktWert * p.taktEinheit); + laeufe = Math.max(1, Math.floor(p.tage * 1440 / intervallMin)); + } + return (p.anzahl * laeufe + p.manuell) * saetze.live; + } + if (typ === "recherche") { + return p.anzahl * rechercheNeu() + p.vertiefungen * saetze.research; + } + return (p.auto ? autoAufrufe() : p.aufrufe) * saetze.neben; + } + + // ---------- Posten-Felder ---------- + + function feldHtml(typ, i, name, labelText, hinweis, wert, min, max) { + var id = "vr-" + typ + "-" + i + "-" + name; + return '
" + + '
'; + } + + function taktHtml(i, p) { + var idW = "vr-live-" + i + "-taktwert"; + var idE = "vr-live-" + i + "-takteinheit"; + var intervallMin = p.taktWert * p.taktEinheit; + var chips = [ + { min: 60, text: "stündlich" }, + { min: 720, text: "halbtäglich" }, + { min: 1440, text: "täglich" } + ].map(function (c) { + return '"; + }).join(""); + return '
' + + '' + + '' + + '
" + + '
' + chips + "
"; + } + + function postenFelder(typ, i, p) { + if (typ === "live") { + var idManuell = "vr-live-" + i + "-nurmanuell"; + var html = feldHtml(typ, i, "anzahl", "Anzahl Lagen", "", p.anzahl, 1, 200) + + '
' + + '
"; + if (!p.nurManuell) { + html += taktHtml(i, p) + + feldHtml(typ, i, "tage", "Laufzeit in Tagen", "wie lange aktualisiert wird", p.tage, 1, 365); + } + html += feldHtml(typ, i, "manuell", "Aktualisierungen von Hand", "gesamt für diesen Posten", p.manuell, 0, 1000); + return html; + } + if (typ === "recherche") { + return feldHtml(typ, i, "anzahl", "Anzahl Recherchen", "", p.anzahl, 1, 200) + + feldHtml(typ, i, "vertiefungen", "Aktualisierungen von Hand", "gesamt für diesen Posten", p.vertiefungen, 0, 1000); + } + var idAuto = "vr-neben-" + i + "-auto"; + var idAnz = "vr-neben-" + i + "-aufrufe"; + return '
' + + '
" + + '
' + + '
"; + } + + function renderBlock(typ) { + var el = document.getElementById("vr-liste-" + typ); + if (!el) return; + el.innerHTML = bloecke[typ].map(function (p, i) { + return '
' + + '
' + + 'Posten ' + (i + 1) + "" + + '' + + "
" + + postenFelder(typ, i, p) + + '
= ' + fmt(postenCredits(typ, p)) + " Credits
" + + "
"; + }).join(""); + } + + // ---------- Regler (frei einstellbare Saetze) ---------- + + function reglerHtml(satz, kicker, label, min, max) { + return '
' + + '
' + kicker + "
" + + '
' + label + "
" + + '
' + saetze[satz] + " Credits
" + + '' + + (satz === "live" || satz === "research" ? '
' : "") + + '
' + + (satz === "neben" ? "Nebenverbrauch real 0,02 bis 0,10 $ je Aufruf. 1 Credit ist die kleinste sinnvolle Einheit." : "Empfehlung wird geladen…") + + "
" + + "
"; + } + + function updateWerte() { + var wL = document.getElementById("vr-wert-live"); if (wL) wL.textContent = saetze.live; + var wR = document.getElementById("vr-wert-research"); if (wR) wR.textContent = saetze.research; + var wN = document.getElementById("vr-wert-neben"); if (wN) wN.textContent = saetze.neben; + var noteL = document.getElementById("vr-note-live"); + if (noteL) noteL.textContent = "Gilt für Anlegen und jede Aktualisierung, gleicher Satz."; + var noteR = document.getElementById("vr-note-research"); + if (noteR) noteR.textContent = "Anlegen einer Recherche = 3 × " + saetze.research + " = " + rechercheNeu() + " Credits."; + } + + var empfDaten = null; + + function renderEmpfehlung() { + if (!empfDaten) return; + ["live", "research"].forEach(function (satz) { + var el = document.getElementById("vr-empf-" + satz); + if (!el) return; + var t = (empfDaten.typen || {})[satz]; + if (!t || t.empfohlen == null || !t.median_usd) { + el.textContent = "Keine Kostendaten der letzten " + empfDaten.days + " Tage."; + return; + } + el.innerHTML = "Empfohlen " + t.empfohlen + " · Median " + fmtUsd(t.median_usd) + + " aus " + fmt(t.n) + " Läufen (" + empfDaten.days + " Tage) " + + ''; + }); + } + + function ladeEmpfehlung() { + var token = null; + try { token = localStorage.getItem("token"); } catch (e) {} + fetch("/api/pricing/cost-basis?days=30", { + headers: token ? { "Authorization": "Bearer " + token } : {} + }).then(function (r) { + if (!r.ok) throw new Error("HTTP " + r.status); + return r.json(); + }).then(function (data) { + empfDaten = data; + renderEmpfehlung(); + }).catch(function () { + ["live", "research"].forEach(function (satz) { + var el = document.getElementById("vr-empf-" + satz); + if (el) el.textContent = "Empfehlung derzeit nicht verfügbar."; + }); + }); + } + + // ---------- Gesamtrechnung ---------- + + function calc() { + var total = 0; + var zeilen = []; + BLOCK_REIHENFOLGE.forEach(function (typ) { + bloecke[typ].forEach(function (p, i) { + var c = postenCredits(typ, p); + total += c; + zeilen.push('
' + BLOCK_NAMEN[typ] + " " + (i + 1) + "" + fmt(c) + "
"); + }); + }); + var posten = document.getElementById("vr-posten-zeilen"); + if (posten) posten.innerHTML = zeilen.length ? zeilen.join("") : '
Noch keine Posten0
'; + var totalEl = document.getElementById("vr-total"); + if (totalEl) totalEl.innerHTML = fmt(total) + "Credits insgesamt"; + var usdEl = document.getElementById("vr-usd"); + if (usdEl) usdEl.textContent = fmtUsd(total * USD); + syncNebenAnzeige(); + } + + function syncNebenAnzeige() { + bloecke.neben.forEach(function (p, i) { + if (!p.auto) return; + var wrap = document.querySelector('#vr-liste-neben .vr-posten[data-i="' + i + '"]'); + if (!wrap) return; + var input = wrap.querySelector('input[data-feld="aufrufe"]'); + if (input) input.value = autoAufrufe(); + var sumEl = wrap.querySelector("[data-sum]"); + if (sumEl) sumEl.textContent = fmt(postenCredits("neben", p)); + }); + } + + function alleSummenNeu() { + BLOCK_REIHENFOLGE.forEach(function (typ) { + bloecke[typ].forEach(function (p, i) { + var wrap = document.querySelector("#vr-liste-" + typ + ' .vr-posten[data-i="' + i + '"]'); + if (!wrap) return; + var sumEl = wrap.querySelector("[data-sum]"); + if (sumEl) sumEl.textContent = fmt(postenCredits(typ, p)); + }); + }); + } + + function satzGesetzt() { + updateWerte(); + alleSummenNeu(); + calc(); + } + + function wertUebernehmen(e) { + var wrap = e.target.closest(".vr-posten"); + var name = e.target.getAttribute("data-feld"); + if (!wrap || !name) return; + var typ = wrap.getAttribute("data-typ"); + var i = parseInt(wrap.getAttribute("data-i"), 10); + if (name === "auto") { + if (!e.target.checked) bloecke.neben[i].aufrufe = autoAufrufe(); + bloecke.neben[i].auto = e.target.checked; + renderBlock("neben"); + calc(); + return; + } + if (name === "nurManuell") { + bloecke.live[i].nurManuell = e.target.checked; + renderBlock("live"); + calc(); + return; + } + bloecke[typ][i][name] = parseFloat(e.target.value) || 0; + var sumEl = wrap.querySelector("[data-sum]"); + if (sumEl) sumEl.textContent = fmt(postenCredits(typ, bloecke[typ][i])); + calc(); + } + + function normalisiereTakt(el) { + var wrap = el.closest(".vr-posten"); + if (!wrap) return; + var i = parseInt(wrap.getAttribute("data-i"), 10); + var p = bloecke.live[i]; + if (!p || p.nurManuell || p.taktWert <= 0) return; + var intervallMin = p.taktWert * p.taktEinheit; + if (intervallMin >= MINIMUM_TAKT_MINUTEN) return; + p.taktWert = Math.ceil((MINIMUM_TAKT_MINUTEN / p.taktEinheit) * 100) / 100; + var input = wrap.querySelector('input[data-feld="taktWert"]'); + if (input) input.value = p.taktWert; + var sumEl = wrap.querySelector("[data-sum]"); + if (sumEl) sumEl.textContent = fmt(postenCredits("live", p)); + calc(); + } + + // ---------- Styles ---------- + + var STYLES = + ".vr-kopf { margin-bottom: 16px; }" + + ".vr-kopf h2 { margin: 0 0 4px; font-size: 20px; }" + + ".vr-kopf p { margin: 0; font-size: 13px; color: var(--text-secondary); }" + + ".vr-regler-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; margin-bottom: 10px; }" + + ".vr-regler { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }" + + ".vr-regler-kicker { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); margin-bottom: 6px; }" + + ".vr-regler-label { font-size: 14px; font-weight: 600; margin-bottom: 6px; }" + + ".vr-regler-wert { font-size: 30px; font-weight: 700; color: var(--accent); line-height: 1.1; margin-bottom: 8px; }" + + ".vr-regler-wert small { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-left: 4px; }" + + ".vr-regler input[type=range] { width: 100%; accent-color: var(--accent); cursor: pointer; }" + + ".vr-note { font-size: 12px; color: var(--text-secondary); margin-top: 6px; }" + + ".vr-empf { font-size: 12px; color: var(--text-muted); margin-top: 8px; line-height: 1.5; }" + + ".vr-empf b { color: var(--accent); }" + + ".vr-uebernehmen { margin-left: 4px; padding: 2px 9px; font-family: inherit; font-size: 11px; background: var(--bg-tertiary); color: var(--accent); border: 1px solid var(--accent); border-radius: 999px; cursor: pointer; }" + + ".vr-uebernehmen:hover { background: var(--accent); color: var(--bg-primary); }" + + ".vr-basis { font-size: 13px; color: var(--text-secondary); margin: 0 0 20px; }" + + ".vr-calc { display: grid; grid-template-columns: 1fr 320px; gap: 20px; align-items: start; }" + + "@media (max-width: 900px) { .vr-calc { grid-template-columns: 1fr; } }" + + ".vr-block { margin-bottom: 24px; }" + + ".vr-block-title { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); margin: 0 0 10px; }" + + ".vr-posten { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; margin-bottom: 10px; }" + + ".vr-head { display: flex; align-items: center; margin-bottom: 10px; }" + + ".vr-title { font-weight: 600; font-size: 13px; }" + + ".vr-remove { margin-left: auto; width: 26px; height: 26px; background: transparent; border: 1px solid var(--border); border-radius: 6px; color: var(--text-secondary); font-size: 15px; line-height: 1; cursor: pointer; }" + + ".vr-remove:hover { border-color: var(--danger); color: var(--danger); }" + + ".vr-field { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; }" + + ".vr-field label { font-size: 13px; color: var(--text-primary); }" + + ".vr-field label small { display: block; font-size: 11px; color: var(--text-muted); }" + + ".vr-field input[type=number], .vr-field select { width: 110px; flex: none; padding: 6px 8px; background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border); border-radius: 6px; font-family: inherit; font-size: 13px; }" + + ".vr-field input[type=number]::-webkit-outer-spin-button, .vr-field input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }" + + ".vr-field input[type=number] { -moz-appearance: textfield; appearance: textfield; }" + + ".vr-field input[type=checkbox] { width: 16px; height: 16px; flex: none; accent-color: var(--accent); cursor: pointer; }" + + ".vr-field input:disabled { opacity: 0.55; }" + + ".vr-field input:focus, .vr-field select:focus { outline: none; border-color: var(--accent); }" + + ".vr-takt { display: flex; gap: 6px; flex: none; }" + + ".vr-takt input { width: 58px; }" + + ".vr-takt select { width: 96px; }" + + ".vr-schnell { display: flex; gap: 6px; justify-content: flex-end; flex-wrap: wrap; margin: -2px 0 10px; }" + + ".vr-schnell button { padding: 2px 10px; font-family: inherit; font-size: 11px; background: var(--bg-tertiary); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 999px; cursor: pointer; }" + + ".vr-schnell button:hover, .vr-schnell button.aktiv { border-color: var(--accent); color: var(--accent); }" + + ".vr-sum { border-top: 1px solid var(--bg-tertiary); margin-top: 10px; padding-top: 8px; text-align: right; font-size: 13px; color: var(--text-secondary); }" + + ".vr-sum b { color: var(--accent); font-size: 14px; }" + + ".vr-add { width: 100%; padding: 9px; background: transparent; border: 1px dashed var(--accent); border-radius: 8px; color: var(--accent); font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer; }" + + ".vr-add:hover { background: rgba(240, 180, 41, 0.08); }" + + ".vr-result { background: var(--bg-secondary); border: 1px solid var(--accent); border-radius: 8px; padding: 16px; position: sticky; top: 16px; }" + + ".vr-total { font-size: 30px; font-weight: 700; color: var(--accent); line-height: 1.1; }" + + ".vr-total small { display: block; font-size: 12px; font-weight: 500; color: var(--text-secondary); margin-top: 4px; }" + + ".vr-row { display: flex; justify-content: space-between; font-size: 13px; padding: 5px 0; border-bottom: 1px solid var(--bg-tertiary); }" + + ".vr-row:last-child { border-bottom: none; }" + + ".vr-row span:last-child { font-weight: 600; font-variant-numeric: tabular-nums; }" + + ".vr-sep { margin: 12px 0; border: none; border-top: 1px solid var(--border); }"; + + // ---------- Aufbau ---------- + + function sektionHtml() { + return '
' + + "

Verbrauchsrechner

" + + "

Sätze frei einstellbar. Die Empfehlung stammt aus den echten API-Kosten der letzten 30 Tage (Median je Aktion geteilt durch 0,20 USD).

" + + "
" + + '
' + + reglerHtml("live", "Live-Monitoring", "Anlegen und Aktualisierung", 1, 100) + + reglerHtml("research", "Recherche", "Je Aktualisierung (Durchlauf)", 1, 100) + + reglerHtml("neben", "Nebenkosten", "Je Aufruf", 1, 10) + + "
" + + '

1 Credit = 0,20 USD Selbstkosten.

' + + '
' + + "
" + + '

Live-Monitoring

' + + '
' + + '

Recherche

' + + '
' + + '

Nebenkosten

' + + '
' + + "
" + + '
' + + '
0Credits insgesamt
' + + '
' + + '
' + + '
' + + '
Selbstkosten0 $
' + + "
" + + "
"; + } + + function init() { + var nav = document.querySelector("nav.nav-tabs"); + var content = document.querySelector(".app-content"); + if (!nav || !content || document.getElementById("sec-rechner")) return; + + var styleEl = document.createElement("style"); + styleEl.textContent = STYLES; + document.head.appendChild(styleEl); + + var sektion = document.createElement("div"); + sektion.className = "section"; + sektion.id = "sec-rechner"; + sektion.innerHTML = sektionHtml(); + content.appendChild(sektion); + + var btn = document.createElement("button"); + btn.className = "nav-tab"; + btn.setAttribute("data-section", "rechner"); + btn.textContent = "Verbrauchsrechner"; + nav.appendChild(btn); + + btn.addEventListener("click", function () { + document.querySelectorAll("nav.nav-tabs .nav-tab").forEach(function (t) { t.classList.remove("active"); }); + btn.classList.add("active"); + document.querySelectorAll(".app-content > .section").forEach(function (s) { s.classList.remove("active"); }); + sektion.classList.add("active"); + var detail = document.getElementById("orgDetail"); + if (detail) detail.classList.remove("active"); + }); + + nav.addEventListener("click", function (e) { + var ziel = e.target.closest(".nav-tab"); + if (!ziel || ziel === btn) return; + btn.classList.remove("active"); + sektion.classList.remove("active"); + }); + + // Regler-Slider live in die Saetze uebernehmen. + sektion.addEventListener("input", function (e) { + if (e.target.type === "range") { + var satz = e.target.getAttribute("data-satz"); + saetze[satz] = parseInt(e.target.value, 10) || 1; + satzGesetzt(); + return; + } + if (e.target.tagName === "SELECT") return; + wertUebernehmen(e); + }); + + sektion.addEventListener("change", function (e) { + var name = e.target.getAttribute("data-feld"); + if (e.target.tagName === "SELECT") wertUebernehmen(e); + if (name === "taktWert" || name === "taktEinheit") normalisiereTakt(e.target); + }); + + sektion.addEventListener("click", function (e) { + var uebBtn = e.target.closest("[data-uebernehmen]"); + if (uebBtn) { + var s = uebBtn.getAttribute("data-uebernehmen"); + var w = parseInt(uebBtn.getAttribute("data-wert"), 10); + if (w > 0) { + saetze[s] = w; + var slider = document.getElementById("vr-slider-" + s); + if (slider) { + if (w > parseInt(slider.max, 10)) slider.max = w; + slider.value = w; + } + satzGesetzt(); + } + return; + } + var taktBtn = e.target.closest("[data-takt]"); + if (taktBtn) { + var wrapT = taktBtn.closest(".vr-posten"); + var iT = parseInt(wrapT.getAttribute("data-i"), 10); + var minuten = parseInt(taktBtn.getAttribute("data-takt"), 10); + var pT = bloecke.live[iT]; + if (minuten % 1440 === 0) { pT.taktWert = minuten / 1440; pT.taktEinheit = 1440; } + else if (minuten % 60 === 0) { pT.taktWert = minuten / 60; pT.taktEinheit = 60; } + else { pT.taktWert = minuten; pT.taktEinheit = 1; } + renderBlock("live"); + calc(); + return; + } + var addBtn = e.target.closest("[data-add]"); + if (addBtn) { + var typNeu = addBtn.getAttribute("data-add"); + bloecke[typNeu].push(neuerPosten(typNeu)); + renderBlock(typNeu); + calc(); + return; + } + var removeBtn = e.target.closest("[data-remove]"); + if (removeBtn) { + var wrap = removeBtn.closest(".vr-posten"); + var typ = wrap.getAttribute("data-typ"); + bloecke[typ].splice(parseInt(wrap.getAttribute("data-i"), 10), 1); + renderBlock(typ); + calc(); + } + }); + + updateWerte(); + renderBlock("live"); + renderBlock("recherche"); + renderBlock("neben"); + calc(); + ladeEmpfehlung(); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})();