- 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>
452 lines
19 KiB
JavaScript
452 lines
19 KiB
JavaScript
/* =====================================================================
|
||
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 []; }
|
||
});
|
||
})();
|