- 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>
893 lines
39 KiB
JavaScript
893 lines
39 KiB
JavaScript
/* =====================================================================
|
||
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;
|
||
});
|
||
})();
|