aether-framework/resources/[aether]/aether-police/html/js/module/fahrzeug.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

452 lines
19 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: Fuhrpark
Zeigt Standort, Tankstand, Zustand und letzten Fahrer — das sind
gemeldete Tatsachen. Zuweisung, Status und Wartung setzt immer ein
Beamter; es gibt keine automatische Fahrzeugvergabe.
===================================================================== */
"use strict";
(() => {
const { sicher, zeit, aufruf, meldung, fenster, bestaetigen, $, anClient } = App;
const z = {
daten: null,
offen: null,
filterAbteilung: "",
filterStatus: "",
filterWartung: "",
suche: "",
};
const statusArt = {
verfuegbar: "gut", im_einsatz: "warn",
werkstatt: "kritisch", ausser_dienst: "neutral",
};
const wartungArt = { ok: "gut", faellig: "warn", werkstatt: "kritisch" };
/* ---- Bausteine --------------------------------------------------- */
/** Balken für Tank oder Zustand */
function balken(anteil, art) {
const wert = Math.max(0, Math.min(100, anteil));
const farbe = wert < 20 ? "var(--kritisch)" : wert < 50 ? "var(--warn)" : "var(--gut)";
return `<div class="fz-balken" title="${art}: ${Math.round(wert)} %">
<i style="width:${wert}%;background:${farbe}"></i>
</div>`;
}
function statusName(id) {
return (z.daten.statusliste && z.daten.statusliste[id]) || id;
}
function wartungName(id) {
return (z.daten.wartungsliste && z.daten.wartungsliste[id]) || id;
}
function abteilungName(id) {
const treffer = (z.daten.abteilungen || []).find((a) => a.id === id);
return treffer ? treffer.name : id;
}
/* ---- Übersicht ---------------------------------------------------- */
function uebersicht(behaelter) {
const d = z.daten;
behaelter.innerHTML = `
<div class="seiten-kopf">
<div>
<h2>Fuhrpark</h2>
<p>Standort, Tank und Zustand werden gemeldet — Zuweisung und Status setzt ein Beamter.</p>
</div>
${d.rechte.wartung ? `<button class="knopf klein" id="fz-neu">+ Fahrzeug aufnehmen</button>` : ""}
</div>
<div class="raster k4" style="margin-bottom:14px">
<div class="kennzahl gut"><div class="kz-titel">Verfügbar</div>
<div class="kz-wert">${d.bestand.verfuegbar || 0}</div></div>
<div class="kennzahl warn"><div class="kz-titel">Im Einsatz</div>
<div class="kz-wert">${d.bestand.im_einsatz || 0}</div></div>
<div class="kennzahl ${d.bestand.werkstatt ? "hoch" : ""}"><div class="kz-titel">Werkstatt</div>
<div class="kz-wert">${d.bestand.werkstatt || 0}</div></div>
<div class="kennzahl ${d.wartungFaellig ? "warn" : ""}"><div class="kz-titel">Wartung offen</div>
<div class="kz-wert">${d.wartungFaellig}</div></div>
</div>
<div class="mdt-suchleiste" style="margin-bottom:14px">
<input id="fz-suche" type="search" placeholder="Kennzeichen, Bezeichnung oder Modell …"
value="${sicher(z.suche)}">
<select id="fz-abteilung" class="katalog-suche">
<option value="">Alle Abteilungen</option>
${(d.abteilungen || []).map((a) =>
`<option value="${sicher(a.id)}" ${z.filterAbteilung === a.id ? "selected" : ""}>${sicher(a.name)}</option>`).join("")}
</select>
<select id="fz-status" class="katalog-suche">
<option value="">Alle Status</option>
${Object.keys(d.statusliste || {}).map((s) =>
`<option value="${s}" ${z.filterStatus === s ? "selected" : ""}>${sicher(d.statusliste[s])}</option>`).join("")}
</select>
</div>
<div class="karte">
${d.fahrzeuge.length ? `<div class="liste">${d.fahrzeuge.map((f) => `
<div class="liste-zeile treffer fz-zeile" data-fahrzeug="${f.id}">
<span class="kennzeichen">${sicher(f.kennzeichen)}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher(f.bezeichnung)}
${f.wartung !== "ok"
? `<span class="marker ${wartungArt[f.wartung]}">${sicher(wartungName(f.wartung))}</span>` : ""}
</div>
<div class="lz-unter">
${sicher(f.modell)} · ${sicher(abteilungName(f.abteilung))}
${f.z_nummer ? " · zugewiesen: " + sicher(f.z_nummer) : ""}
</div>
<div class="fz-werte">
<span class="fz-wert">⛽ ${Math.round(f.tank)} %</span>${balken(f.tank, "Tank")}
<span class="fz-wert">🔧 ${Math.round(f.zustand / 10)} %</span>${balken(f.zustand / 10, "Zustand")}
</div>
</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:5px">
<span class="marker ${statusArt[f.status]}">${sicher(statusName(f.status))}</span>
${f.pos_x ? `<button class="knopf leise klein" data-weg="${f.pos_x},${f.pos_y}">Standort</button>` : ""}
</div>
</div>`).join("")}</div>`
: `<div class="liste-leer">Kein Fahrzeug im Fuhrpark.</div>`}
</div>`;
const neu = $("fz-neu");
if (neu) neu.addEventListener("click", () => anlegenDialog(behaelter));
const suchfeld = $("fz-suche");
suchfeld.addEventListener("keydown", (e) => {
if (e.key === "Enter") { z.suche = suchfeld.value.trim(); rendern(behaelter); }
});
$("fz-abteilung").addEventListener("change", (e) => {
z.filterAbteilung = e.target.value; rendern(behaelter);
});
$("fz-status").addEventListener("change", (e) => {
z.filterStatus = e.target.value; rendern(behaelter);
});
// Standort auf der Karte markieren, ohne die Zeile zu öffnen
behaelter.querySelectorAll("[data-weg]").forEach((b) =>
b.addEventListener("click", (e) => {
e.stopPropagation();
const [x, y] = b.dataset.weg.split(",").map(Number);
anClient("wegpunkt", { x, y });
}));
behaelter.querySelectorAll("[data-fahrzeug]").forEach((el) =>
el.addEventListener("click", () => oeffne(behaelter, +el.dataset.fahrzeug)));
}
/* ---- Einzelansicht ------------------------------------------------ */
function einzelAnsicht(behaelter, d) {
const f = d.fahrzeug;
behaelter.innerHTML = `
<div class="seiten-kopf">
<div>
<h2>${sicher(f.bezeichnung)}</h2>
<p>${sicher(f.kennzeichen)} · ${sicher(f.modell)} · ${sicher(abteilungName(f.abteilung))}</p>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="knopf leise klein" id="fz-zurueck">‹ Zurück</button>
${f.pos_x ? `<button class="knopf leise klein" id="fz-standort">📍 Standort</button>` : ""}
${d.rechte.zuweisen ? `<button class="knopf leise klein" id="fz-status-setzen">Status</button>` : ""}
${d.rechte.wartung ? `<button class="knopf leise klein" id="fz-wartung">Wartung</button>` : ""}
${d.rechte.zuweisen ? `<button class="knopf klein" id="fz-zuweisen">Zuweisen</button>` : ""}
</div>
</div>
${f.wartung !== "ok" ? `
<div class="warnung normal">
<span class="w-symbol">🔧</span>
<span><b>${sicher(wartungName(f.wartung))}</b> — das Fahrzeug sollte vor dem
nächsten Einsatz geprüft werden.</span>
</div>` : ""}
<div class="raster k2" style="margin-top:14px">
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">Zustand</span>
<span class="marker ${statusArt[f.status]}">${sicher(statusName(f.status))}</span>
</div>
<div class="fz-anzeige">
<div class="fz-anzeige-zeile">
<span>⛽ Tankstand</span>
<b>${Math.round(f.tank)} %</b>
</div>
${balken(f.tank, "Tank")}
<div class="fz-anzeige-zeile" style="margin-top:12px">
<span>🔧 Karosserie</span>
<b>${Math.round(f.zustand / 10)} %</b>
</div>
${balken(f.zustand / 10, "Zustand")}
</div>
<div class="akte-zeilen" style="grid-template-columns:auto 1fr;margin-top:16px">
<span>Wartung</span><b>${sicher(wartungName(f.wartung))}</b>
<span>Zugewiesen an</span><b>${f.z_nummer
? sicher(f.z_nummer + " " + (f.z_vorname || "") + " " + (f.z_nachname || ""))
: "— frei —"}</b>
<span>Letzter Fahrer</span><b>${f.l_nummer
? sicher(f.l_nummer + " " + (f.l_vorname || "") + " " + (f.l_nachname || ""))
: "— nicht erfasst —"}</b>
<span>Standort</span><b>${f.pos_x
? `${Math.round(f.pos_x)}, ${Math.round(f.pos_y)}`
: "— nicht gemeldet —"}</b>
</div>
<div class="grundsatz">
<span>ℹ</span>
<div>Tank, Zustand, Standort und letzter Fahrer werden vom Fahrzeug
<b>gemeldet</b>. Status, Zuweisung und Wartung setzt ausschließlich ein Beamter.</div>
</div>
</div>
<div class="karte">
<div class="karte-kopf">
<span class="karte-titel">Verwaltung</span>
${d.rechte.wartung && !f.zugewiesen_an
? `<button class="knopf leise klein" id="fz-entfernen">Ausmustern</button>` : ""}
</div>
${f.zugewiesen_an ? `
<div class="liste">
<div class="liste-zeile">
<span class="marker info">${sicher(f.z_nummer || "")}</span>
<div class="lz-haupt">
<div class="lz-titel">${sicher((f.z_vorname || "") + " " + (f.z_nachname || ""))}</div>
<div class="lz-unter">Fahrzeug ist dieser Person zugewiesen</div>
</div>
${d.rechte.zuweisen
? `<button class="knopf leise klein" id="fz-freigeben">Freigeben</button>` : ""}
</div>
</div>`
: `<div class="liste-leer">Das Fahrzeug ist keiner Person zugewiesen.</div>`}
</div>
<div class="karte" style="grid-column:1/-1">
<div class="karte-kopf"><span class="karte-titel">Vorgangshistorie</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.dienstnummer || "")}
${sicher((h.firstname || "") + " " + (h.lastname || ""))} · ${zeit(h.am)}
${h.grund ? " · " + sicher(h.grund) : ""}</div>
</div>
</div>`).join("")}</div>`
: `<div class="liste-leer">Keine Einträge.</div>`}
</div>
</div>`;
verdrahteEinzel(behaelter, d);
}
function verdrahteEinzel(behaelter, d) {
const f = d.fahrzeug;
$("fz-zurueck").addEventListener("click", () => { z.offen = null; rendern(behaelter); });
const standort = $("fz-standort");
if (standort) standort.addEventListener("click", () =>
anClient("wegpunkt", { x: f.pos_x, y: f.pos_y }));
const zuweisen = $("fz-zuweisen");
if (zuweisen) zuweisen.addEventListener("click", () => {
const beamte = d.beamteImDienst || [];
fenster({
titel: "Fahrzeug zuweisen",
inhalt: `
<p style="margin:0 0 12px;font-size:13px;line-height:1.6">
<b>${sicher(f.bezeichnung)}</b> (${sicher(f.kennzeichen)})</p>
${beamte.length ? `<div class="liste">${beamte.map((b) =>
`<div class="liste-zeile treffer" data-wahl="${b.charakterId}">
<span class="marker neutral">${sicher(b.dienstnummer)}</span>
<div class="lz-haupt"><div class="lz-titel">${sicher(b.name)}</div>
<div class="lz-unter">${sicher(b.rolleName)} · ${sicher(b.abteilung)}</div></div>
</div>`).join("")}</div>`
: `<div class="liste-leer">Niemand im Dienst.</div>`}
<div class="feld" style="margin-top:12px"><label>Vermerk</label>
<input id="fzz-grund" placeholder="optional"></div>`,
nachOeffnen: (el) => {
el.querySelectorAll("[data-wahl]").forEach((zeile) =>
zeile.addEventListener("click", async () => {
try {
await aufruf("fahrzeug", "zuweisen", {
id: f.id, charakterId: +zeile.dataset.wahl,
grund: el.querySelector("#fzz-grund").value.trim(),
});
el.remove(); meldung("Zugewiesen.", "gut"); oeffne(behaelter, f.id);
} catch (fehler) { meldung(fehler.message, "fehler"); }
}));
},
});
});
const freigeben = $("fz-freigeben");
if (freigeben) freigeben.addEventListener("click", async () => {
try {
await aufruf("fahrzeug", "zuweisen", { id: f.id, freigeben: true });
meldung("Zuweisung aufgehoben."); oeffne(behaelter, f.id);
} catch (fehler) { meldung(fehler.message, "fehler"); }
});
const statusSetzen = $("fz-status-setzen");
if (statusSetzen) statusSetzen.addEventListener("click", () => {
fenster({
titel: "Status setzen",
inhalt: `
<div class="feld"><label>Neuer Status</label>
<select id="fzs-status">
${Object.keys(d.statusliste).map((s) =>
`<option value="${s}" ${f.status === s ? "selected" : ""}>${sicher(d.statusliste[s])}</option>`).join("")}
</select></div>
<div class="feld"><label>Vermerk</label>
<input id="fzs-grund" placeholder="optional"></div>`,
knoepfe: [
{ text: "Abbrechen", klick: (_, zu) => zu() },
{ text: "Speichern", klick: async (el, zu) => {
try {
await aufruf("fahrzeug", "status", {
id: f.id, status: el.querySelector("#fzs-status").value,
grund: el.querySelector("#fzs-grund").value.trim(),
});
zu(); meldung("Status gesetzt.", "gut"); oeffne(behaelter, f.id);
} catch (fehler) { meldung(fehler.message, "fehler"); }
}},
],
});
});
const wartung = $("fz-wartung");
if (wartung) wartung.addEventListener("click", () => {
fenster({
titel: "Wartungsstatus",
inhalt: `
<div class="feld"><label>Zustand der Wartung</label>
<select id="fzw-wartung">
${Object.keys(d.wartungsliste).map((w) =>
`<option value="${w}" ${f.wartung === w ? "selected" : ""}>${sicher(d.wartungsliste[w])}</option>`).join("")}
</select></div>
<div class="feld"><label>Vermerk</label>
<input id="fzw-vermerk" placeholder="Was wurde festgestellt?"></div>`,
knoepfe: [
{ text: "Abbrechen", klick: (_, zu) => zu() },
{ text: "Speichern", klick: async (el, zu) => {
try {
await aufruf("fahrzeug", "wartung", {
id: f.id, wartung: el.querySelector("#fzw-wartung").value,
vermerk: el.querySelector("#fzw-vermerk").value.trim(),
});
zu(); meldung("Wartungsstatus gesetzt.", "gut"); oeffne(behaelter, f.id);
} catch (fehler) { meldung(fehler.message, "fehler"); }
}},
],
});
});
const entfernen = $("fz-entfernen");
if (entfernen) entfernen.addEventListener("click", () => {
bestaetigen({
titel: "Fahrzeug ausmustern",
gefahr: true,
inhalt: `
<p style="margin:0 0 12px;font-size:13px;line-height:1.6">
<b>${sicher(f.bezeichnung)}</b> (${sicher(f.kennzeichen)}) wird aus dem Fuhrpark
entfernt. Der Vorgang bleibt im Protokoll sichtbar.</p>
<div class="feld"><label>Grund</label>
<input id="fze-grund" placeholder="z. B. Totalschaden, verkauft"></div>`,
bestaetigenText: "Ausmustern",
bestaetigt: async (el, zu) => {
try {
await aufruf("fahrzeug", "entfernen", {
id: f.id, grund: el.querySelector("#fze-grund").value.trim(),
});
zu(); meldung("Ausgemustert."); z.offen = null; rendern(behaelter);
} catch (fehler) { meldung(fehler.message, "fehler"); }
},
});
});
}
/* ---- Anlegen ------------------------------------------------------ */
function anlegenDialog(behaelter) {
fenster({
titel: "Fahrzeug aufnehmen",
inhalt: `
<div class="raster k2" style="gap:10px">
<div class="feld" style="margin:0"><label>Kennzeichen</label>
<input id="fn-kz" placeholder="z. B. LSPD001"></div>
<div class="feld" style="margin:0"><label>Modell (Spawnname)</label>
<input id="fn-modell" placeholder="z. B. police3"></div>
</div>
<div class="feld"><label>Bezeichnung</label>
<input id="fn-bez" placeholder="z. B. Streifenwagen 1-ADAM-12"></div>
<div class="feld"><label>Abteilung</label>
<select id="fn-abteilung">
${(z.daten.abteilungen || []).map((a) =>
`<option value="${sicher(a.id)}">${sicher(a.name)}</option>`).join("")}
</select></div>
<div class="feld-hinweis">
Das Kennzeichen muss mit dem im Spiel übereinstimmen — nur dann können
Tank, Zustand und Standort gemeldet werden.
</div>`,
knoepfe: [
{ text: "Abbrechen", klick: (_, zu) => zu() },
{ text: "Aufnehmen", klick: async (el, zu) => {
try {
const ergebnis = await aufruf("fahrzeug", "anlegen", {
kennzeichen: el.querySelector("#fn-kz").value.trim(),
modell: el.querySelector("#fn-modell").value.trim(),
bezeichnung: el.querySelector("#fn-bez").value.trim(),
abteilung: el.querySelector("#fn-abteilung").value,
});
zu(); meldung("Fahrzeug aufgenommen.", "gut");
oeffne(behaelter, ergebnis.id);
} catch (fehler) { meldung(fehler.message, "fehler"); }
}},
],
});
}
/* ---- Laden -------------------------------------------------------- */
async function oeffne(behaelter, fahrzeugId) {
try {
const d = await aufruf("fahrzeug", "oeffnen", { id: fahrzeugId });
z.offen = fahrzeugId;
einzelAnsicht(behaelter, d);
} catch (f) { meldung(f.message, "fehler"); }
}
async function rendern(behaelter) {
try {
z.daten = await aufruf("fahrzeug", "liste", {
abteilung: z.filterAbteilung, status: z.filterStatus,
wartung: z.filterWartung, begriff: z.suche,
});
if (z.offen) await oeffne(behaelter, z.offen);
else uebersicht(behaelter);
} catch (f) {
behaelter.innerHTML = `<div class="seiten-kopf"><div><h2>Fuhrpark</h2>
<p>Konnte nicht geladen werden: ${sicher(f.message)}</p></div></div>`;
}
}
App.registriereAnsicht("fahrzeug", { rendern });
App.registriereSuche(async (begriff) => {
try {
const d = await aufruf("fahrzeug", "liste", { begriff });
return (d.fahrzeuge || []).slice(0, 5).map((f) => ({
bereich: "Dienstfahrzeug", titel: f.bezeichnung,
unter: f.kennzeichen + " · " + f.modell,
}));
} catch (e) { return []; }
});
})();