aether-framework/resources/[aether]/aether-police/html/js/module/mdt.js
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark,
  Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI)
- aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource,
  an aether-core gekoppelt (Rechte über aether_users.admin_group), mit
  WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU)
- Prototypen admin/ vervollständigt
- SQL-Schema aether_admin.sql
- Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:55:32 +02:00

893 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* =====================================================================
aether-police / Ansicht: MDT
Drei Reiter: Personen, Fahrzeuge, Akten.
Die Ansicht zeigt und dokumentiert — sie bewertet nichts und löst
keine Maßnahme aus. Jede Änderung verlangt eine Bestätigung.
===================================================================== */
"use strict";
(() => {
const { sicher, zeit, aufruf, meldung, fenster, bestaetigen, $, anClient } = App;
/* Zustand der Ansicht */
const z = {
reiter: "personen", // personen | fahrzeuge | akten
suche: "",
treffer: [],
offenePerson: null, // geladene Personenakte
offenesFahrzeug: null,
offeneAkte: null,
akteFilter: { typ: "", status: "" },
stammdaten: null, // Aktenarten, Beteiligtenrollen
};
/* ---- kleine Bausteine ------------------------------------------- */
const gefahrText = ["keine", "gering", "erhöht", "hoch"];
/** Marke für die Gefahrenstufe einer Person */
function gefahrMarke(stufe) {
const s = Number(stufe) || 0;
if (s === 0) return "";
const art = s >= 3 ? "kritisch" : s === 2 ? "warn" : "neutral";
return `<span class="marker ${art}">Gefahr: ${gefahrText[s]}</span>`;
}
function reiterLeiste() {
const reiter = [
{ id: "personen", name: "Personen", symbol: "👤" },
{ id: "fahrzeuge", name: "Fahrzeuge", symbol: "🚗" },
{ id: "akten", name: "Akten", symbol: "🗂" },
];
return `<div class="mdt-reiter">${reiter.map((r) =>
`<button data-reiter="${r.id}" class="${r.id === z.reiter ? "aktiv" : ""}">
${r.symbol} ${r.name}
</button>`).join("")}</div>`;
}
/* =====================================================================
PERSONEN
===================================================================== */
function personenTrefferListe() {
if (!z.treffer.length) {
return `<div class="liste-leer">${z.suche ? "Keine Treffer." : "Suchbegriff eingeben — Name, Geburtsdatum oder Telefonnummer."}</div>`;
}
return `<div class="liste">${z.treffer.map((p) =>
`<div class="liste-zeile treffer" data-person="${p.id}">
<div class="lz-haupt">
<div class="lz-titel">
${sicher(p.firstname + " " + p.lastname)}
${p.fahndung > 0 ? '<span class="marker kritisch">Fahndung</span>' : ""}
${gefahrMarke(p.gefahrenstufe)}
</div>
<div class="lz-unter">
geb. ${sicher(String(p.dob || "").slice(0, 10))}
${p.telefon ? " · ☎ " + sicher(p.telefon) : ""}
${p.job ? " · " + sicher(p.job) : ""}
</div>
</div>
<button class="knopf leise klein">Akte öffnen</button>
</div>`).join("")}</div>`;
}
/** Zeichnet eine vollständige Personenakte */
function personenAkte(d) {
const p = d.person;
const lizenzName = {
perso: "Personalausweis", fs: "Führerschein", ws: "Waffenschein",
gk: "Gesundheitskarte", vk: "Visitenkarte",
};
return `
<div class="akte-kopf">
<div class="akte-bild">${p.lichtbild
? `<img src="${sicher(p.lichtbild)}" alt="Lichtbild">`
: `<span class="kein-bild">Kein Lichtbild</span>`}</div>
<div class="akte-kopf-text">
<h3>${sicher(p.firstname + " " + p.lastname)}</h3>
<div class="akte-zeilen">
<span>Geboren</span><b>${sicher(String(p.dob || "").slice(0, 10))}</b>
<span>Geschlecht</span><b>${sicher(p.gender || "—")}</b>
<span>Herkunft</span><b>${sicher(p.nationality || "—")}</b>
<span>Beruf</span><b>${sicher(p.job || "—")}</b>
<span>Telefon</span><b>${sicher(d.telefon || "—")}</b>
<span>Fingerabdruck</span><b>${sicher(p.fingerabdruck || "nicht erfasst")}</b>
<span>DNA</span><b>${sicher(p.dna || "nicht erfasst")}</b>
</div>
</div>
<div class="akte-kopf-aktionen">
<button class="knopf leise klein" id="p-notiz">+ Notiz</button>
<button class="knopf leise klein" id="p-warn">⚠ Warnhinweis</button>
<button class="knopf leise klein" id="p-ed">🔬 ED-Daten</button>
<button class="knopf klein" id="p-akte-neu">🗂 Akte anlegen</button>
</div>
</div>
${p.warnhinweis ? `
<div class="warnung hoch">
<span class="w-symbol">⚠</span>
<span><b>Warnhinweis:</b> ${sicher(p.warnhinweis)} — Gefahrenstufe ${gefahrText[Number(p.gefahrenstufe) || 0]}</span>
</div>` : ""}
${d.fahndung ? `
<div class="warnung hoch">
<span class="w-symbol">🔍</span>
<span><b>Zur Fahndung ausgeschrieben:</b> ${sicher(d.fahndung.grund)}
(${sicher(d.fahndung.gefahr)})</span>
</div>` : ""}
${d.gewahrsam ? `
<div class="warnung normal">
<span class="w-symbol">🔒</span>
<span><b>In Gewahrsam</b> — ${sicher(d.gewahrsam.grund || "ohne Grundangabe")};
Frist ${Number(d.gewahrsam.rest) <= 0 ? "abgelaufen" : "noch " + d.gewahrsam.rest + " Minuten"}.
Eine Entlassung entscheidet ein Beamter.</span>
</div>` : ""}
<div class="raster k2" style="margin-top:14px">
<div class="karte">
<div class="karte-kopf"><span class="karte-titel">Dokumente & Lizenzen</span></div>
${d.lizenzen.length ? `<div class="liste">${d.lizenzen.map((l) =>
`<div class="liste-zeile">
<div class="lz-haupt">
<div class="lz-titel">${sicher(lizenzName[l.typ] || l.typ)}</div>
<div class="lz-unter">
${l.nummer ? sicher(l.nummer) : "—"}
${l.klassen ? " · Klassen " + sicher(l.klassen.join(", ")) : ""}
${l.gueltigBis ? " · gültig bis " + sicher(l.gueltigBis) : ""}
</div>
</div>
<span class="marker ${l.entzogen ? "kritisch" : l.gueltig ? "gut" : "warn"}">
${l.entzogen ? "entzogen" : l.gueltig ? "gültig" : "abgelaufen"}
</span>
</div>`).join("")}</div>`
: `<div class="liste-leer">Keine Dokumente hinterlegt.</div>`}
</div>
<div class="karte">
<div class="karte-kopf"><span class="karte-titel">Bekannte Fahrzeuge</span></div>
${d.fahrzeuge.length ? `<div class="liste">${d.fahrzeuge.map((f) =>
`<div class="liste-zeile treffer" data-fahrzeug="${sicher(f.plate)}">
<div class="lz-haupt">
<div class="lz-titel">${sicher(f.name || f.model)}
${f.fahndung > 0 ? '<span class="marker kritisch">Fahndung</span>' : ""}</div>
<div class="lz-unter">${sicher(f.plate)} · ${sicher(f.state === "impound" ? "sichergestellt" : f.state === "out" ? "unterwegs" : "eingeparkt")}</div>
</div>
</div>`).join("")}</div>`
: `<div class="liste-leer">Keine Fahrzeuge zugeordnet.</div>`}
</div>
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">Vorgänge & Anzeigen</span>
<span class="marker neutral">${d.anzeigen.length}</span>
</div>
${d.anzeigen.length ? `<div class="liste">${d.anzeigen.map((a) =>
`<div class="liste-zeile">
<div class="lz-haupt">
<div class="lz-titel">${sicher((a.delikte || []).map((x) => x.name).join(", ") || "Anzeige")}</div>
<div class="lz-unter">${zeit(a.erstellt_am)}
${a.status === "bestaetigt" ? ` · ${a.haft_final} Min · ${App.euro(a.geld_final)}` : ""}</div>
</div>
<span class="marker ${a.status === "bestaetigt" ? "info" : a.status === "verworfen" ? "neutral" : "warn"}">
${sicher(a.status)}
</span>
</div>`).join("")}</div>`
: `<div class="liste-leer">Keine Vorgänge erfasst.</div>`}
</div>
<div class="karte">
<div class="karte-kopf"><span class="karte-titel">Verknüpfte Akten</span></div>
${d.akten.length ? `<div class="liste">${d.akten.map((a) =>
`<div class="liste-zeile treffer" data-akte="${a.id}">
<div class="lz-haupt">
<div class="lz-titel">${sicher(a.titel)}</div>
<div class="lz-unter">${sicher(a.aktenzeichen)} · ${sicher(a.typ)}
${a.rolle ? " · " + sicher(a.rolle) : ""}</div>
</div>
<span class="marker ${a.status === "geschlossen" ? "neutral" : "warn"}">${sicher(a.status)}</span>
</div>`).join("")}</div>`
: `<div class="liste-leer">Keine Akten verknüpft.</div>`}
</div>
<div class="karte">
<div class="karte-kopf"><span class="karte-titel">Bekannte Kontakte</span></div>
${d.kontakte.length ? `<div class="liste">${d.kontakte.map((k) =>
`<div class="liste-zeile ${k.charakterId ? "treffer" : ""}" ${k.charakterId ? `data-person="${k.charakterId}"` : ""}>
<div class="lz-haupt">
<div class="lz-titel">${sicher(k.name)}</div>
<div class="lz-unter">☎ ${sicher(k.number)}</div>
</div>
${k.charakterId ? '<span class="marker info">bekannt</span>' : ""}
</div>`).join("")}</div>`
: `<div class="liste-leer">Keine Kontakte bekannt.</div>`}
</div>
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">Notizen</span>
<span class="marker neutral">${d.notizen.length}</span>
</div>
${d.notizen.length ? `<div class="liste">${d.notizen.map((n) =>
`<div class="liste-zeile">
<div class="lz-haupt">
<div class="lz-titel" style="font-weight:600">${sicher(n.text)}</div>
<div class="lz-unter">${sicher((n.firstname || "") + " " + (n.lastname || ""))} · ${zeit(n.am)}</div>
</div>
<button class="knopf leise klein" data-notiz-weg="${n.id}">✕</button>
</div>`).join("")}</div>`
: `<div class="liste-leer">Keine Notizen.</div>`}
</div>
</div>
`;
}
/* ---- Aktionen an der Personenakte -------------------------------- */
function verdrahtePersonenakte(behaelter, charakterId) {
// Notiz hinzufügen
const notizKnopf = $("p-notiz");
if (notizKnopf) notizKnopf.addEventListener("click", () => {
fenster({
titel: "Notiz hinzufügen",
inhalt: `<div class="feld">
<label>Vermerk</label>
<textarea id="nz-text" placeholder="Sachlicher Vermerk zur Person …"></textarea>
<div class="feld-hinweis">Notizen sind für alle Beamten sichtbar und werden protokolliert.</div>
</div>`,
knoepfe: [
{ text: "Abbrechen", klick: (_, zu) => zu() },
{ text: "Speichern", art: "", klick: async (el, zu) => {
const text = el.querySelector("#nz-text").value.trim();
if (text.length < 3) { meldung("Bitte einen Text eingeben.", "fehler"); return; }
try {
await aufruf("mdt", "personNotiz", { charakterId, text });
zu(); meldung("Notiz gespeichert.", "gut");
oeffnePerson(behaelter, charakterId);
} catch (f) { meldung(f.message, "fehler"); }
}},
],
});
});
// Notiz entfernen
behaelter.querySelectorAll("[data-notiz-weg]").forEach((b) => {
b.addEventListener("click", async (e) => {
e.stopPropagation();
try {
await aufruf("mdt", "personNotizLoeschen", { id: +b.dataset.notizWeg });
oeffnePerson(behaelter, charakterId);
} catch (f) { meldung(f.message, "fehler"); }
});
});
// Warnhinweis setzen
const warnKnopf = $("p-warn");
if (warnKnopf) warnKnopf.addEventListener("click", () => {
const p = z.offenePerson.person;
bestaetigen({
titel: "Warnhinweis festlegen",
inhalt: `
<div class="feld">
<label>Hinweis (leer = entfernen)</label>
<input id="wh-text" value="${sicher(p.warnhinweis || "")}"
placeholder="z. B. gewaltbereit, führt Waffen mit">
</div>
<div class="feld">
<label>Gefahrenstufe</label>
<select id="wh-stufe">
${[0, 1, 2, 3].map((s) =>
`<option value="${s}" ${Number(p.gefahrenstufe) === s ? "selected" : ""}>${s} — ${gefahrText[s]}</option>`
).join("")}
</select>
</div>
<div class="feld">
<label>Begründung</label>
<input id="wh-grund" placeholder="Worauf stützt sich die Einschätzung?">
</div>`,
bestaetigenText: "Warnhinweis setzen",
bestaetigt: async (el, zu) => {
try {
await aufruf("mdt", "personWarnhinweis", {
charakterId,
hinweis: el.querySelector("#wh-text").value.trim(),
stufe: +el.querySelector("#wh-stufe").value,
grund: el.querySelector("#wh-grund").value.trim(),
});
zu(); meldung("Warnhinweis aktualisiert.", "gut");
oeffnePerson(behaelter, charakterId);
} catch (f) { meldung(f.message, "fehler"); }
},
});
});
// Erkennungsdienstliche Daten
const edKnopf = $("p-ed");
if (edKnopf) edKnopf.addEventListener("click", () => {
const p = z.offenePerson.person;
fenster({
titel: "Erkennungsdienstliche Daten",
inhalt: `
<div class="feld"><label>Fingerabdruck-Kennung</label>
<input id="ed-finger" value="${sicher(p.fingerabdruck || "")}"></div>
<div class="feld"><label>DNA-Kennung</label>
<input id="ed-dna" value="${sicher(p.dna || "")}"></div>
<div class="feld"><label>Lichtbild (Bild-Adresse)</label>
<input id="ed-bild" value="${sicher(p.lichtbild || "")}" placeholder="https://…">
<div class="feld-hinweis">Die Aufnahme selbst erfolgt im Rollenspiel — hier wird nur dokumentiert.</div>
</div>`,
knoepfe: [
{ text: "Abbrechen", klick: (_, zu) => zu() },
{ text: "Speichern", klick: async (el, zu) => {
try {
await aufruf("mdt", "personStammdaten", {
charakterId,
fingerabdruck: el.querySelector("#ed-finger").value.trim(),
dna: el.querySelector("#ed-dna").value.trim(),
lichtbild: el.querySelector("#ed-bild").value.trim(),
});
zu(); meldung("Daten gespeichert.", "gut");
oeffnePerson(behaelter, charakterId);
} catch (f) { meldung(f.message, "fehler"); }
}},
],
});
});
// Akte direkt zu dieser Person anlegen
const akteKnopf = $("p-akte-neu");
if (akteKnopf) akteKnopf.addEventListener("click", () =>
akteAnlegenDialog(behaelter, { typ: "person", id: charakterId }));
// Sprünge in andere Akten
behaelter.querySelectorAll("[data-akte]").forEach((el) =>
el.addEventListener("click", () => { z.reiter = "akten"; oeffneAkte(behaelter, +el.dataset.akte); }));
behaelter.querySelectorAll("[data-fahrzeug]").forEach((el) =>
el.addEventListener("click", () => { z.reiter = "fahrzeuge"; oeffneFahrzeug(behaelter, el.dataset.fahrzeug); }));
behaelter.querySelectorAll("[data-person]").forEach((el) =>
el.addEventListener("click", () => oeffnePerson(behaelter, +el.dataset.person)));
}
async function oeffnePerson(behaelter, charakterId) {
try {
const daten = await aufruf("mdt", "personAkte", { charakterId });
z.offenePerson = daten;
z.reiter = "personen";
zeichne(behaelter, personenAkte(daten), () => verdrahtePersonenakte(behaelter, charakterId), true);
} catch (f) { meldung(f.message, "fehler"); }
}
/* =====================================================================
FAHRZEUGE
===================================================================== */
function fahrzeugTrefferListe() {
if (!z.treffer.length) {
return `<div class="liste-leer">${z.suche ? "Keine Treffer." : "Kennzeichen, Modell oder Halter eingeben."}</div>`;
}
return `<div class="liste">${z.treffer.map((f) =>
`<div class="liste-zeile treffer" data-fahrzeug="${sicher(f.plate)}">
<span class="kennzeichen">${sicher(f.plate)}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(f.name || f.model)}
${f.fahndung > 0 ? '<span class="marker kritisch">Fahndung</span>' : ""}
${f.state === "impound" ? '<span class="marker warn">sichergestellt</span>' : ""}
</div>
<div class="lz-unter">Halter: ${sicher((f.firstname || "unbekannt") + " " + (f.lastname || ""))}</div>
</div>
<button class="knopf leise klein">Details</button>
</div>`).join("")}</div>`;
}
function fahrzeugAkte(d) {
const f = d.fahrzeug;
const zustandText = { in: "eingeparkt", out: "unterwegs", impound: "sichergestellt" };
return `
<div class="akte-kopf">
<div class="akte-bild kennzeichen-gross">${sicher(f.plate)}</div>
<div class="akte-kopf-text">
<h3>${sicher(f.name || f.model)}</h3>
<div class="akte-zeilen">
<span>Modell</span><b>${sicher(f.model)}</b>
<span>Klasse</span><b>${sicher(f.klasse || "—")}</b>
<span>Farbe</span><b>${f.farbe ? `<span class="farbtupfer" style="background:${sicher(f.farbe)}"></span> ${sicher(f.farbe)}` : "—"}</b>
<span>Status</span><b>${sicher(zustandText[f.state] || f.state)}</b>
<span>Standort</span><b>${sicher(f.garage || "—")}</b>
<span>Kilometerstand</span><b>${sicher(f.mileage || 0)} km</b>
</div>
</div>
<div class="akte-kopf-aktionen">
${f.owner ? `<button class="knopf leise klein" data-person="${f.owner}">👤 Halterakte</button>` : ""}
<button class="knopf klein" id="v-akte-neu">🗂 Akte anlegen</button>
</div>
</div>
${d.fahndung ? `
<div class="warnung hoch">
<span class="w-symbol">🔍</span>
<span><b>Fahrzeug zur Fahndung ausgeschrieben:</b> ${sicher(d.fahndung.grund)}</span>
</div>` : ""}
<div class="raster k2" style="margin-top:14px">
<div class="karte">
<div class="karte-kopf"><span class="karte-titel">Halter</span></div>
${f.owner ? `<div class="liste">
<div class="liste-zeile treffer" data-person="${f.owner}">
<div class="lz-haupt">
<div class="lz-titel">${sicher((f.firstname || "") + " " + (f.lastname || ""))}</div>
<div class="lz-unter">geb. ${sicher(String(f.dob || "").slice(0, 10))}</div>
</div>
<button class="knopf leise klein">Akte öffnen</button>
</div></div>`
: `<div class="liste-leer">Kein Halter eingetragen.</div>`}
</div>
<div class="karte">
<div class="karte-kopf"><span class="karte-titel">Verknüpfte Akten</span></div>
${d.akten.length ? `<div class="liste">${d.akten.map((a) =>
`<div class="liste-zeile treffer" data-akte="${a.id}">
<div class="lz-haupt">
<div class="lz-titel">${sicher(a.titel)}</div>
<div class="lz-unter">${sicher(a.aktenzeichen)} · ${sicher(a.typ)}</div>
</div>
</div>`).join("")}</div>`
: `<div class="liste-leer">Keine Akten verknüpft.</div>`}
</div>
<div class="karte" style="grid-column:1/-1">
<div class="karte-kopf"><span class="karte-titel">Historie</span></div>
${d.historie.length ? `<div class="liste">${d.historie.map((h) =>
`<div class="liste-zeile">
<div class="lz-haupt">
<div class="lz-titel">${sicher(h.aktion)}</div>
<div class="lz-unter">${sicher((h.firstname || "") + " " + (h.lastname || ""))}
${h.dienstnummer ? " · " + sicher(h.dienstnummer) : ""} · ${zeit(h.am)}
${h.grund ? " · " + sicher(h.grund) : ""}</div>
</div>
</div>`).join("")}</div>`
: `<div class="liste-leer">Keine dokumentierten Vorgänge.</div>`}
</div>
</div>
`;
}
async function oeffneFahrzeug(behaelter, kennzeichen) {
try {
const daten = await aufruf("mdt", "fahrzeugAkte", { kennzeichen });
z.offenesFahrzeug = daten;
z.reiter = "fahrzeuge";
zeichne(behaelter, fahrzeugAkte(daten), () => {
behaelter.querySelectorAll("[data-person]").forEach((el) =>
el.addEventListener("click", () => oeffnePerson(behaelter, +el.dataset.person)));
behaelter.querySelectorAll("[data-akte]").forEach((el) =>
el.addEventListener("click", () => oeffneAkte(behaelter, +el.dataset.akte)));
const neu = $("v-akte-neu");
if (neu) neu.addEventListener("click", () =>
akteAnlegenDialog(behaelter, { typ: "fahrzeug", id: daten.fahrzeug.plate }));
}, true);
} catch (f) { meldung(f.message, "fehler"); }
}
/* =====================================================================
AKTEN
===================================================================== */
function aktenListe(akten) {
if (!akten.length) return `<div class="liste-leer">Keine Akten gefunden.</div>`;
const statusArt = { offen: "warn", pruefung: "info", geschlossen: "neutral" };
return `<div class="liste">${akten.map((a) =>
`<div class="liste-zeile treffer" data-akte="${a.id}">
<span class="marker neutral">${sicher(a.typ)}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(a.titel)}</div>
<div class="lz-unter">${sicher(a.aktenzeichen)} ·
${sicher((a.firstname || "") + " " + (a.lastname || ""))} · ${zeit(a.erstellt_am)}
${a.version > 1 ? ` · Version ${a.version}` : ""}</div>
</div>
<span class="marker ${statusArt[a.status] || "neutral"}">${sicher(a.status)}</span>
</div>`).join("")}</div>`;
}
function akteAnsicht(d) {
const a = d.akte;
const statusArt = { offen: "warn", pruefung: "info", geschlossen: "neutral" };
return `
<div class="akte-kopf">
<div class="akte-kopf-text" style="flex:1">
<h3>${sicher(a.titel)}</h3>
<div class="akte-zeilen">
<span>Aktenzeichen</span><b>${sicher(a.aktenzeichen)}</b>
<span>Art</span><b>${sicher(a.typ)}</b>
<span>Angelegt von</span><b>${sicher((a.firstname || "") + " " + (a.lastname || ""))}</b>
<span>Angelegt am</span><b>${zeit(a.erstellt_am)}</b>
<span>Version</span><b>${sicher(a.version)}</b>
</div>
</div>
<div class="akte-kopf-aktionen">
<span class="marker ${statusArt[a.status] || "neutral"}" style="align-self:flex-start">${sicher(a.status)}</span>
${d.darfBearbeiten ? `<button class="knopf leise klein" id="a-bearbeiten">✎ Bearbeiten</button>` : ""}
${d.darfBearbeiten ? `<button class="knopf leise klein" id="a-verknuepfen">🔗 Verknüpfen</button>` : ""}
${d.darfSchliessen && a.status !== "geschlossen"
? `<button class="knopf klein" id="a-schliessen">Akte schließen</button>` : ""}
</div>
</div>
<div class="raster k2" style="margin-top:14px">
<div class="karte" style="grid-column:1/-1">
<div class="karte-kopf"><span class="karte-titel">Sachverhalt</span></div>
<div class="akte-inhalt">${a.inhalt ? sicher(a.inhalt).replace(/\n/g, "<br>") : "<i>Kein Inhalt hinterlegt.</i>"}</div>
</div>
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">Verknüpfungen</span>
<span class="marker neutral">${d.verknuepfungen.length}</span>
</div>
${d.verknuepfungen.length ? `<div class="liste">${d.verknuepfungen.map((v) =>
`<div class="liste-zeile">
<span class="marker neutral">${sicher(v.typ)}</span>
<div class="lz-haupt">
<div class="lz-titel treffer-text"
${v.typ === "person" ? `data-person="${sicher(v.ziel_id)}"` : ""}
${v.typ === "fahrzeug" ? `data-fahrzeug="${sicher(v.ziel_id)}"` : ""}>
${sicher(v.bezeichnung || v.ziel_id)}
</div>
${v.rolle ? `<div class="lz-unter">${sicher(v.rolle)}</div>` : ""}
</div>
${d.darfBearbeiten ? `<button class="knopf leise klein" data-vk-weg="${v.id}">✕</button>` : ""}
</div>`).join("")}</div>`
: `<div class="liste-leer">Nichts verknüpft.</div>`}
</div>
<div class="karte">
<div class="karte-kopf"><span class="karte-titel">Änderungshistorie</span></div>
${d.versionen.length ? `<div class="liste">${d.versionen.map((v) =>
`<div class="liste-zeile">
<span class="marker neutral">v${sicher(v.version)}</span>
<div class="lz-haupt">
<div class="lz-titel" style="font-weight:600">${sicher(v.grund || "ohne Angabe")}</div>
<div class="lz-unter">${sicher((v.firstname || "") + " " + (v.lastname || ""))} · ${zeit(v.geaendert_am)}</div>
</div>
</div>`).join("")}</div>`
: `<div class="liste-leer">Keine Änderungen.</div>`}
</div>
</div>
`;
}
async function oeffneAkte(behaelter, akteId) {
try {
const daten = await aufruf("mdt", "akteOeffnen", { id: akteId });
z.offeneAkte = daten;
z.reiter = "akten";
zeichne(behaelter, akteAnsicht(daten), () => verdrahteAkte(behaelter, daten), true);
} catch (f) { meldung(f.message, "fehler"); }
}
function verdrahteAkte(behaelter, d) {
const a = d.akte;
const bearbeiten = $("a-bearbeiten");
if (bearbeiten) bearbeiten.addEventListener("click", () => {
fenster({
titel: "Akte bearbeiten",
inhalt: `
<div class="feld"><label>Titel</label>
<input id="ab-titel" value="${sicher(a.titel)}"></div>
<div class="feld"><label>Sachverhalt</label>
<textarea id="ab-inhalt" style="min-height:180px">${sicher(a.inhalt || "")}</textarea></div>
<div class="feld"><label>Grund der Änderung</label>
<input id="ab-grund" placeholder="Wird in der Historie festgehalten">
<div class="feld-hinweis">Jede Änderung erzeugt eine neue Version. Die alte bleibt nachvollziehbar.</div>
</div>`,
knoepfe: [
{ text: "Abbrechen", klick: (_, zu) => zu() },
{ text: "Speichern", klick: async (el, zu) => {
try {
await aufruf("mdt", "akteBearbeiten", {
id: a.id,
titel: el.querySelector("#ab-titel").value.trim(),
inhalt: el.querySelector("#ab-inhalt").value,
grund: el.querySelector("#ab-grund").value.trim(),
});
zu(); meldung("Akte gespeichert.", "gut");
oeffneAkte(behaelter, a.id);
} catch (f) { meldung(f.message, "fehler"); }
}},
],
});
});
const verknuepfen = $("a-verknuepfen");
if (verknuepfen) verknuepfen.addEventListener("click", () => {
const rollen = (z.stammdaten && z.stammdaten.rollen) || [];
fenster({
titel: "Verknüpfung hinzufügen",
inhalt: `
<div class="feld"><label>Art</label>
<select id="vk-typ">
<option value="person">Person (Charakter-ID)</option>
<option value="fahrzeug">Fahrzeug (Kennzeichen)</option>
<option value="beweis">Beweis (Kennung)</option>
<option value="einsatz">Einsatz (Nummer)</option>
</select></div>
<div class="feld"><label>Kennung</label>
<input id="vk-ziel" placeholder="z. B. 42 oder LS-4821"></div>
<div class="feld"><label>Beteiligung</label>
<select id="vk-rolle">
<option value="">— keine Angabe —</option>
${rollen.map((r) => `<option value="${sicher(r.id)}">${sicher(r.name)}</option>`).join("")}
</select></div>`,
knoepfe: [
{ text: "Abbrechen", klick: (_, zu) => zu() },
{ text: "Verknüpfen", klick: async (el, zu) => {
try {
await aufruf("mdt", "akteVerknuepfen", {
akteId: a.id,
typ: el.querySelector("#vk-typ").value,
zielId: el.querySelector("#vk-ziel").value.trim(),
rolle: el.querySelector("#vk-rolle").value || null,
});
zu(); meldung("Verknüpfung angelegt.", "gut");
oeffneAkte(behaelter, a.id);
} catch (f) { meldung(f.message, "fehler"); }
}},
],
});
});
behaelter.querySelectorAll("[data-vk-weg]").forEach((b) =>
b.addEventListener("click", async () => {
try {
await aufruf("mdt", "akteVerknuepfungLoesen", { id: +b.dataset.vkWeg });
oeffneAkte(behaelter, a.id);
} catch (f) { meldung(f.message, "fehler"); }
}));
const schliessen = $("a-schliessen");
if (schliessen) schliessen.addEventListener("click", () => {
bestaetigen({
titel: "Akte schließen",
gefahr: true,
inhalt: `<p style="margin:0 0 12px;font-size:13px;line-height:1.6">
Die Akte <b>${sicher(a.aktenzeichen)}</b> wird als abgeschlossen gekennzeichnet.
Es wird festgehalten, dass <b>du</b> diese Entscheidung getroffen hast.</p>
<div class="feld"><label>Grund</label><input id="as-grund" placeholder="Abschlussvermerk"></div>`,
bestaetigenText: "Akte schließen",
bestaetigt: async (el, zu) => {
try {
await aufruf("mdt", "akteStatus", {
id: a.id, status: "geschlossen",
grund: el.querySelector("#as-grund").value.trim(),
});
zu(); meldung("Akte geschlossen.", "gut");
oeffneAkte(behaelter, a.id);
} catch (f) { meldung(f.message, "fehler"); }
},
});
});
behaelter.querySelectorAll("[data-person]").forEach((el) =>
el.addEventListener("click", () => oeffnePerson(behaelter, +el.dataset.person)));
behaelter.querySelectorAll("[data-fahrzeug]").forEach((el) =>
el.addEventListener("click", () => oeffneFahrzeug(behaelter, el.dataset.fahrzeug)));
}
/** Dialog zum Anlegen einer Akte, optional gleich verknüpft */
function akteAnlegenDialog(behaelter, verknuepfung) {
const arten = (z.stammdaten && z.stammdaten.aktenarten) || [];
const rollen = (z.stammdaten && z.stammdaten.rollen) || [];
fenster({
titel: "Akte anlegen",
inhalt: `
<div class="feld"><label>Aktenart</label>
<select id="an-typ">
${arten.map((a) => `<option value="${sicher(a.id)}">${sicher(a.name)}</option>`).join("")}
</select></div>
<div class="feld"><label>Titel</label>
<input id="an-titel" placeholder="Kurze, eindeutige Bezeichnung"></div>
<div class="feld"><label>Sachverhalt</label>
<textarea id="an-inhalt" placeholder="Was ist geschehen?"></textarea></div>
${verknuepfung ? `
<div class="feld"><label>Beteiligung der verknüpften ${verknuepfung.typ === "person" ? "Person" : "Sache"}</label>
<select id="an-rolle">
<option value="">— keine Angabe —</option>
${rollen.map((r) => `<option value="${sicher(r.id)}">${sicher(r.name)}</option>`).join("")}
</select></div>` : ""}`,
knoepfe: [
{ text: "Abbrechen", klick: (_, zu) => zu() },
{ text: "Akte anlegen", klick: async (el, zu) => {
try {
const ergebnis = await aufruf("mdt", "akteErstellen", {
typ: el.querySelector("#an-typ").value,
titel: el.querySelector("#an-titel").value.trim(),
inhalt: el.querySelector("#an-inhalt").value,
verknuepfeTyp: verknuepfung && verknuepfung.typ,
verknuepfeId: verknuepfung && verknuepfung.id,
verknuepfeRolle: verknuepfung ? (el.querySelector("#an-rolle") || {}).value : null,
});
zu();
meldung("Akte " + ergebnis.aktenzeichen + " angelegt.", "gut");
oeffneAkte(behaelter, ergebnis.id);
} catch (f) { meldung(f.message, "fehler"); }
}},
],
});
}
/* =====================================================================
Gerüst
===================================================================== */
/**
* Zeichnet den MDT-Rahmen mit Inhalt.
* @param {boolean} detail Wenn true, wird ein Zurück-Knopf gezeigt.
*/
function zeichne(behaelter, inhalt, nachher, detail) {
const suchfeld = {
personen: "Name, Geburtsdatum oder Telefonnummer …",
fahrzeuge: "Kennzeichen, Modell oder Halter …",
akten: "Aktenzeichen oder Titel …",
}[z.reiter];
behaelter.innerHTML = `
<div class="seiten-kopf">
<div><h2>MDT</h2><p>Personen, Fahrzeuge und Akten</p></div>
${z.reiter === "akten"
? `<button class="knopf klein" id="mdt-akte-neu">+ Neue Akte</button>` : ""}
</div>
${reiterLeiste()}
${detail
? `<button class="knopf leise klein" id="mdt-zurueck" style="margin-bottom:14px">‹ Zurück zur Suche</button>`
: `<div class="mdt-suchleiste">
<input id="mdt-suche" type="search" placeholder="${suchfeld}" value="${sicher(z.suche)}">
<button class="knopf" id="mdt-suchen">Suchen</button>
${z.reiter === "akten" ? `
<select id="mdt-filter-typ">
<option value="">Alle Arten</option>
${((z.stammdaten && z.stammdaten.aktenarten) || []).map((a) =>
`<option value="${sicher(a.id)}" ${z.akteFilter.typ === a.id ? "selected" : ""}>${sicher(a.name)}</option>`).join("")}
</select>
<select id="mdt-filter-status">
<option value="">Alle Status</option>
<option value="offen" ${z.akteFilter.status === "offen" ? "selected" : ""}>Offen</option>
<option value="pruefung" ${z.akteFilter.status === "pruefung" ? "selected" : ""}>In Prüfung</option>
<option value="geschlossen" ${z.akteFilter.status === "geschlossen" ? "selected" : ""}>Geschlossen</option>
</select>` : ""}
</div>`}
<div class="karte" style="margin-top:14px">${inhalt}</div>
`;
// Reiter wechseln
behaelter.querySelectorAll("[data-reiter]").forEach((b) =>
b.addEventListener("click", () => {
z.reiter = b.dataset.reiter;
z.suche = ""; z.treffer = [];
rendern(behaelter);
}));
const zurueck = $("mdt-zurueck");
if (zurueck) zurueck.addEventListener("click", () => rendern(behaelter));
const neueAkte = $("mdt-akte-neu");
if (neueAkte) neueAkte.addEventListener("click", () => akteAnlegenDialog(behaelter, null));
// Suche
const suchfeldEl = $("mdt-suche");
if (suchfeldEl) {
const suchen = () => { z.suche = suchfeldEl.value.trim(); ausfuehrenSuche(behaelter); };
suchfeldEl.addEventListener("keydown", (e) => { if (e.key === "Enter") suchen(); });
$("mdt-suchen").addEventListener("click", suchen);
}
["mdt-filter-typ", "mdt-filter-status"].forEach((id) => {
const el = $(id);
if (el) el.addEventListener("change", () => {
z.akteFilter.typ = ($("mdt-filter-typ") || {}).value || "";
z.akteFilter.status = ($("mdt-filter-status") || {}).value || "";
ausfuehrenSuche(behaelter);
});
});
if (nachher) nachher();
}
/** Führt die Suche des aktiven Reiters aus */
async function ausfuehrenSuche(behaelter) {
try {
if (z.reiter === "personen") {
if (!z.suche) { z.treffer = []; zeichne(behaelter, personenTrefferListe(), () => verdrahteTreffer(behaelter)); return; }
const d = await aufruf("mdt", "personSuchen", { begriff: z.suche });
z.treffer = d.treffer;
zeichne(behaelter, personenTrefferListe(), () => verdrahteTreffer(behaelter));
} else if (z.reiter === "fahrzeuge") {
if (!z.suche) { z.treffer = []; zeichne(behaelter, fahrzeugTrefferListe(), () => verdrahteTreffer(behaelter)); return; }
const d = await aufruf("mdt", "fahrzeugSuchen", { begriff: z.suche });
z.treffer = d.treffer;
zeichne(behaelter, fahrzeugTrefferListe(), () => verdrahteTreffer(behaelter));
} else {
const d = await aufruf("mdt", "aktenListe", {
begriff: z.suche, typ: z.akteFilter.typ, status: z.akteFilter.status,
});
zeichne(behaelter, aktenListe(d.akten), () => verdrahteTreffer(behaelter));
}
} catch (f) { meldung(f.message, "fehler"); }
}
function verdrahteTreffer(behaelter) {
behaelter.querySelectorAll("[data-person]").forEach((el) =>
el.addEventListener("click", () => oeffnePerson(behaelter, +el.dataset.person)));
behaelter.querySelectorAll("[data-fahrzeug]").forEach((el) =>
el.addEventListener("click", () => oeffneFahrzeug(behaelter, el.dataset.fahrzeug)));
behaelter.querySelectorAll("[data-akte]").forEach((el) =>
el.addEventListener("click", () => oeffneAkte(behaelter, +el.dataset.akte)));
}
async function rendern(behaelter) {
// Stammdaten einmalig holen (Aktenarten, Beteiligtenrollen)
if (!z.stammdaten) {
try { z.stammdaten = await aufruf("mdt", "stammdaten", {}); } catch (f) { z.stammdaten = {}; }
}
/* Sprungziel eines anderen Moduls auswerten, z. B. aus einer Fahndung.
Es gilt nur einmal und wird danach verworfen. */
const ziel = App.zustand.modulZiel;
if (ziel) {
App.zustand.modulZiel = null;
if (ziel.person) { await oeffnePerson(behaelter, +ziel.person); return; }
if (ziel.fahrzeug) { await oeffneFahrzeug(behaelter, ziel.fahrzeug); return; }
if (ziel.akte) { await oeffneAkte(behaelter, +ziel.akte); return; }
}
await ausfuehrenSuche(behaelter);
}
/* ---- Registrierung ----------------------------------------------- */
App.registriereAnsicht("mdt", { rendern });
/* Globale Suche bedienen */
App.registriereSuche(async (begriff) => {
const treffer = [];
try {
const personen = await aufruf("mdt", "personSuchen", { begriff });
(personen.treffer || []).slice(0, 5).forEach((p) => treffer.push({
bereich: "Person", titel: p.firstname + " " + p.lastname,
unter: "geb. " + String(p.dob || "").slice(0, 10),
}));
} catch (e) { /* Bereich überspringen */ }
try {
const fahrzeuge = await aufruf("mdt", "fahrzeugSuchen", { begriff });
(fahrzeuge.treffer || []).slice(0, 5).forEach((f) => treffer.push({
bereich: "Fahrzeug", titel: f.plate + " · " + (f.name || f.model),
unter: "Halter: " + ((f.firstname || "unbekannt") + " " + (f.lastname || "")),
}));
} catch (e) { /* Bereich überspringen */ }
try {
const akten = await aufruf("mdt", "aktenListe", { begriff });
(akten.akten || []).slice(0, 5).forEach((a) => treffer.push({
bereich: "Akte", titel: a.titel, unter: a.aktenzeichen + " · " + a.typ,
}));
} catch (e) { /* Bereich überspringen */ }
return treffer;
});
})();