Erster Commit
This commit is contained in:
@@ -0,0 +1,777 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Orte & Inhalte verwalten</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||||
a.back:hover { text-decoration: underline; }
|
||||
|
||||
#sidebar {
|
||||
width: 240px;
|
||||
flex-shrink: 0;
|
||||
background: #161616;
|
||||
border-right: 1px solid #333;
|
||||
padding: 16px 0;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
#sidebar .backLink { padding: 0 16px 12px; display: block; }
|
||||
.sidebar-group-label {
|
||||
padding: 10px 16px 4px; font-size: 11px; color: #666;
|
||||
text-transform: uppercase; letter-spacing: 0.5px;
|
||||
}
|
||||
.sidebar-btn {
|
||||
display: block; width: 100%; text-align: left; background: none; border: none;
|
||||
color: #ccc; padding: 8px 16px; cursor: pointer; font-size: 13px;
|
||||
}
|
||||
.sidebar-btn:hover { background: #1f1f1f; }
|
||||
.sidebar-btn.active { background: #2c7a3d; color: white; }
|
||||
|
||||
#main { flex: 1; padding: 24px; max-width: 900px; }
|
||||
h1 { margin-top: 0; font-size: 20px; }
|
||||
|
||||
.panel { display: none; }
|
||||
.panel.active { display: block; }
|
||||
|
||||
.box { background: #1a1a1a; border: 1px solid #333; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 6px; }
|
||||
th, td { text-align: left; padding: 7px 6px; border-bottom: 1px solid #2a2a2a; font-size: 13px; }
|
||||
th { color: #999; font-weight: normal; }
|
||||
|
||||
input, select, textarea {
|
||||
background: #222; border: 1px solid #444; color: #eee;
|
||||
padding: 6px 8px; border-radius: 4px; font-size: 13px; font-family: inherit;
|
||||
}
|
||||
textarea { width: 100%; resize: vertical; }
|
||||
button {
|
||||
background: #2c7a3d; border: none; color: white; padding: 6px 12px;
|
||||
border-radius: 4px; cursor: pointer; font-size: 13px;
|
||||
}
|
||||
button.danger { background: #a83232; }
|
||||
button.secondary { background: #555; }
|
||||
button:hover { opacity: 0.85; }
|
||||
|
||||
.form-grid { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-end; margin-top: 10px; }
|
||||
.form-grid label { display: flex; flex-direction: column; gap: 3px; font-size: 11px; color: #999; }
|
||||
.msg { margin-top: 10px; font-size: 13px; min-height: 18px; }
|
||||
|
||||
.news-columns { display: flex; gap: 16px; }
|
||||
.news-columns > div { flex: 1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="sidebar">
|
||||
<a class="back backLink" href="/admin.html">← Admin-Menü</a>
|
||||
|
||||
<div class="sidebar-group-label">Orte</div>
|
||||
<div id="poiSidebarButtons"></div>
|
||||
<button class="sidebar-btn" data-panel="highway_links">🛣️ Autobahn-Verbindungen</button>
|
||||
|
||||
<div class="sidebar-group-label">Inhalte</div>
|
||||
<button class="sidebar-btn" data-panel="news">📰 News</button>
|
||||
<button class="sidebar-btn" data-panel="changelog">📝 Changelog</button>
|
||||
|
||||
<div class="sidebar-group-label">Integrationen</div>
|
||||
<button class="sidebar-btn" data-panel="discord">🔗 Discord-Webhooks</button>
|
||||
|
||||
<div class="sidebar-group-label">Admin-Dienst</div>
|
||||
<button class="sidebar-btn" data-panel="adminduty">🛡️ Admin-Look</button>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div id="poiPanels"></div>
|
||||
|
||||
<div class="panel" id="panel-highway_links">
|
||||
<h1>🛣️ Autobahn-Verbindungen verwalten</h1>
|
||||
<p style="color:#888; font-size:13px; margin-top:-8px;">
|
||||
Verbindet zwei Punkte (auch auf unterschiedlichen Karten) miteinander. Spieler werden beim
|
||||
Herannahen automatisch dorthin teleportiert - zu Fuß oder mit Auto (das Auto wird mitgenommen).
|
||||
Kein Tastendruck nötig.
|
||||
</p>
|
||||
<div class="box">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Seite A (Welt/X/Y)</th><th>Seite B (Welt/X/Y)</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody id="highwayLinksTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">Neue Verbindung anlegen</h3>
|
||||
<div class="form-grid">
|
||||
<label>Name <input type="text" id="hlName" placeholder="z.B. Autobahn Nord"></label>
|
||||
</div>
|
||||
<div class="form-grid" style="margin-top:10px;">
|
||||
<label>Welt A <input type="text" id="hlWorldA" placeholder="z.B. stadt"></label>
|
||||
<label>X A (Tile) <input type="number" id="hlXA" style="width:80px;"></label>
|
||||
<label>Y A (Tile) <input type="number" id="hlYA" style="width:80px;"></label>
|
||||
</div>
|
||||
<div class="form-grid" style="margin-top:10px;">
|
||||
<label>Welt B <input type="text" id="hlWorldB" placeholder="z.B. neustadt"></label>
|
||||
<label>X B (Tile) <input type="number" id="hlXB" style="width:80px;"></label>
|
||||
<label>Y B (Tile) <input type="number" id="hlYB" style="width:80px;"></label>
|
||||
</div>
|
||||
<button onclick="addHighwayLink()" style="margin-top:12px;">Anlegen</button>
|
||||
<div class="msg" id="hlMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" id="panel-news">
|
||||
<h1>📰 News verwalten</h1>
|
||||
<div class="box">
|
||||
<table>
|
||||
<thead><tr><th>Titel</th><th>Autor</th><th>Datum</th><th></th></tr></thead>
|
||||
<tbody id="newsTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">Neue News</h3>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<label>Titel <input type="text" id="newsTitle"></label>
|
||||
<label>Inhalt <textarea id="newsContent" rows="4"></textarea></label>
|
||||
<button onclick="addNews()" style="align-self:flex-start;">Veröffentlichen</button>
|
||||
</div>
|
||||
<div class="msg" id="newsMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" id="panel-changelog">
|
||||
<h1>📝 Changelog verwalten</h1>
|
||||
<div class="box">
|
||||
<table>
|
||||
<thead><tr><th>Version</th><th>Titel</th><th>Datum</th><th></th></tr></thead>
|
||||
<tbody id="changelogTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">Neuer Eintrag</h3>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<label>Version <input type="text" id="changelogVersion" placeholder="z.B. 1.4.0" style="max-width:150px;"></label>
|
||||
<label>Titel <input type="text" id="changelogTitle"></label>
|
||||
<label>Inhalt <textarea id="changelogContent" rows="4"></textarea></label>
|
||||
<button onclick="addChangelog()" style="align-self:flex-start;">Veröffentlichen</button>
|
||||
</div>
|
||||
<div class="msg" id="changelogMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" id="panel-discord">
|
||||
<h1>🔗 Discord-Webhooks</h1>
|
||||
<p style="color:#888; font-size:13px;">
|
||||
Für jeden Bereich kannst du separat festlegen, in welchen Discord-Kanal die Nachrichten gehen sollen.
|
||||
Webhook-URL bekommst du in Discord unter Servereinstellungen → Integrationen → Webhooks → "Neuer Webhook".
|
||||
Feld leer lassen und speichern entfernt den Webhook wieder.
|
||||
</p>
|
||||
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">📰 News-Kanal</h3>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<input type="text" id="discordUrl_news" placeholder="https://discord.com/api/webhooks/...">
|
||||
<button onclick="saveDiscordWebhook('news')" style="align-self:flex-start;">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="discordMsg_news"></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">📝 Changelog-Kanal</h3>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<input type="text" id="discordUrl_changelog" placeholder="https://discord.com/api/webhooks/...">
|
||||
<button onclick="saveDiscordWebhook('changelog')" style="align-self:flex-start;">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="discordMsg_changelog"></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">⚠️ Neustart-Ankündigungen-Kanal</h3>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<input type="text" id="discordUrl_restart" placeholder="https://discord.com/api/webhooks/...">
|
||||
<button onclick="saveDiscordWebhook('restart')" style="align-self:flex-start;">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="discordMsg_restart"></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">🔧 Wartungsmodus-Kanal</h3>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<input type="text" id="discordUrl_maintenance" placeholder="https://discord.com/api/webhooks/...">
|
||||
<button onclick="saveDiscordWebhook('maintenance')" style="align-self:flex-start;">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="discordMsg_maintenance"></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">🟢 Online-Status-Kanal</h3>
|
||||
<p style="color:#888; font-size:12px; margin-top:-4px;">
|
||||
Postet eine sich selbst aktualisierende Nachricht mit der aktuellen Spielerzahl (bearbeitet
|
||||
immer dieselbe Nachricht statt zu spammen) - aktualisiert bei Login/Logout und alle 5 Minuten.
|
||||
</p>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<input type="text" id="discordUrl_online_status" placeholder="https://discord.com/api/webhooks/...">
|
||||
<button onclick="saveDiscordWebhook('online_status')" style="align-self:flex-start;">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="discordMsg_online_status"></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">🎫 Support-Tickets</h3>
|
||||
<p style="color:#888; font-size:12px; margin-top:-4px;">Meldet jedes neue Support-Ticket.</p>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<input type="text" id="discordUrl_tickets" placeholder="https://discord.com/api/webhooks/...">
|
||||
<button onclick="saveDiscordWebhook('tickets')" style="align-self:flex-start;">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="discordMsg_tickets"></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">💡 Wünsche</h3>
|
||||
<p style="color:#888; font-size:12px; margin-top:-4px;">Meldet jeden neuen Spieler-Wunsch.</p>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<input type="text" id="discordUrl_wishes" placeholder="https://discord.com/api/webhooks/...">
|
||||
<button onclick="saveDiscordWebhook('wishes')" style="align-self:flex-start;">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="discordMsg_wishes"></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">📊 Umfragen</h3>
|
||||
<p style="color:#888; font-size:12px; margin-top:-4px;">Meldet jede neu erstellte Umfrage.</p>
|
||||
<div class="form-grid" style="flex-direction:column; align-items:stretch;">
|
||||
<input type="text" id="discordUrl_surveys" placeholder="https://discord.com/api/webhooks/...">
|
||||
<button onclick="saveDiscordWebhook('surveys')" style="align-self:flex-start;">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="discordMsg_surveys"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" id="panel-adminduty">
|
||||
<h1>🛡️ Admin-Dienst: Aussehen</h1>
|
||||
<p style="color:#888; font-size:13px;">
|
||||
Wenn ein Admin den Admin-Dienstmodus aktiviert, zieht er automatisch die hier festgelegte
|
||||
Kleidung + den Skin an (bildet eine erkennbare "Held"-Figur) und darf Admin-Befehle nutzen.
|
||||
Beim Beenden des Dienstes kommt die vorherige Kleidung automatisch zurück.
|
||||
Leer lassen = kein automatischer Wechsel für dieses Feld.
|
||||
</p>
|
||||
<div class="box">
|
||||
<div class="form-grid">
|
||||
<label>Oberteil
|
||||
<select id="adminShirtSelect"><option value="">- keins -</option></select>
|
||||
</label>
|
||||
<label>Hose
|
||||
<select id="adminPantsSelect"><option value="">- keine -</option></select>
|
||||
</label>
|
||||
<label>Schuhe
|
||||
<select id="adminShoesSelect"><option value="">- keine -</option></select>
|
||||
</label>
|
||||
<label>Helm
|
||||
<select id="adminHelmetSelect"><option value="">- keiner -</option></select>
|
||||
</label>
|
||||
<label>Skin (Figur)
|
||||
<select id="adminSkinSelect"><option value="">- keiner -</option></select>
|
||||
</label>
|
||||
<button onclick="saveAdminLook()">Speichern</button>
|
||||
</div>
|
||||
<div class="msg" id="adminLookMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// -------------------------------------------------------------
|
||||
// ZUGRIFFSSCHUTZ
|
||||
// -------------------------------------------------------------
|
||||
const token = localStorage.getItem("token");
|
||||
// Voll-Admin ODER passende Gruppen-Berechtigung reicht - der Server prüft
|
||||
// das pro Bereich final ab, authFetch() fängt fehlende Rechte sauber ab
|
||||
let isAdmin = localStorage.getItem("isAdmin") === "true";
|
||||
if (!token) {
|
||||
alert("Bitte zuerst einloggen.");
|
||||
location.href = "/index.html";
|
||||
}
|
||||
async function authFetch(url, options = {}) {
|
||||
options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token };
|
||||
const res = await fetch(url, options);
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen.");
|
||||
location.href = "/index.html";
|
||||
throw new Error("Nicht autorisiert");
|
||||
}
|
||||
return res;
|
||||
}
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text == null ? "" : String(text);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// KONFIGURATION ALLER ORTS-TYPEN (config-gesteuert statt 12x Copy-Paste)
|
||||
// -------------------------------------------------------------
|
||||
const POI_CONFIGS = [
|
||||
{ key: "gas_stations", label: "Tankstellen", icon: "⛽", endpoint: "/api/admin/gas_stations", listKey: "gasStations",
|
||||
priceField: "price", priceLabel: "Preis/Liter", priceDefault: 2 },
|
||||
{ key: "repair_shops", label: "Werkstätten", icon: "🔧", endpoint: "/api/admin/repair_shops", listKey: "repairShops",
|
||||
priceField: "pricePerPoint", priceLabel: "Preis/Reparatur-Punkt", priceDefault: 5 },
|
||||
{ key: "garages", label: "Garagen", icon: "🚗", endpoint: "/api/admin/garages", listKey: "garages",
|
||||
extraFields: [{ name: "jobId", label: "Job-ID (optional)", type: "number" }] },
|
||||
{ key: "taxi_stands", label: "Taxistände", icon: "🚕", endpoint: "/api/admin/taxi_stands", listKey: "taxiStands" },
|
||||
{ key: "hospitals", label: "Krankenhäuser", icon: "🏥", endpoint: "/api/admin/hospitals", listKey: "hospitals" },
|
||||
{ key: "prisons", label: "Gefängnisse", icon: "🔒", endpoint: "/api/admin/prisons", listKey: "prisons" },
|
||||
{ key: "fire_stations", label: "Feuerwachen", icon: "🚒", endpoint: "/api/admin/fire_stations", listKey: "stations" },
|
||||
{ key: "impound_lots", label: "Abschlepphöfe", icon: "🚛", endpoint: "/api/admin/impound_lots", listKey: "lots" },
|
||||
{ key: "jobcenters", label: "Jobcenter", icon: "💼", endpoint: "/api/admin/jobcenters", listKey: "jobcenters" },
|
||||
{ key: "insurance_offices", label: "Versicherungsbüros", icon: "🛡️", endpoint: "/api/admin/insurance_offices", listKey: "offices" },
|
||||
{ key: "plate_offices", label: "Zulassungsstellen", icon: "🔖", endpoint: "/api/admin/plate_offices", listKey: "offices" },
|
||||
{ key: "clothing_shops", label: "Kleidungsläden", icon: "👕", endpoint: "/api/admin/clothing_shops", listKey: "shops" },
|
||||
{ key: "trailer_shops", label: "Anhänger-Shops", icon: "🚛", endpoint: "/api/admin/trailer_shops", listKey: "shops" },
|
||||
{ key: "black_market_spots", label: "Hehler (Schwarzmarkt)", icon: "🕶️", endpoint: "/api/admin/black_market_spots", listKey: "shops" }
|
||||
];
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// SIDEBAR + PANELS AUFBAUEN
|
||||
// -------------------------------------------------------------
|
||||
const sidebarButtonsEl = document.getElementById("poiSidebarButtons");
|
||||
const poiPanelsEl = document.getElementById("poiPanels");
|
||||
|
||||
POI_CONFIGS.forEach(cfg => {
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "sidebar-btn";
|
||||
btn.dataset.panel = "poi_" + cfg.key;
|
||||
btn.textContent = `${cfg.icon} ${cfg.label}`;
|
||||
sidebarButtonsEl.appendChild(btn);
|
||||
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "panel";
|
||||
panel.id = "panel-poi_" + cfg.key;
|
||||
panel.innerHTML = `
|
||||
<h1>${cfg.icon} ${cfg.label} verwalten</h1>
|
||||
<div class="box">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Welt</th><th>X</th><th>Y</th>${cfg.priceField ? `<th>${cfg.priceLabel}</th>` : ""}<th></th></tr>
|
||||
</thead>
|
||||
<tbody id="poiTable_${cfg.key}"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h3 style="margin-top:0;">Neuen Eintrag anlegen</h3>
|
||||
<p style="color:#888; font-size:12px; margin-top:-4px;">
|
||||
Schneller geht's meist direkt im Map-Editor per Klick - hier für schnelle Textmengen-Erfassung.
|
||||
</p>
|
||||
<div class="form-grid">
|
||||
<label>Name <input type="text" id="poiName_${cfg.key}"></label>
|
||||
<label>Welt <input type="text" id="poiWorld_${cfg.key}" placeholder="z.B. stadt"></label>
|
||||
<label>X (Tile) <input type="number" id="poiX_${cfg.key}" style="width:80px;"></label>
|
||||
<label>Y (Tile) <input type="number" id="poiY_${cfg.key}" style="width:80px;"></label>
|
||||
${cfg.priceField ? `<label>${cfg.priceLabel} <input type="number" step="0.01" id="poiPrice_${cfg.key}" value="${cfg.priceDefault}" style="width:100px;"></label>` : ""}
|
||||
${(cfg.extraFields || []).map(f => `<label>${f.label} <input type="${f.type}" id="poiExtra_${cfg.key}_${f.name}" style="width:120px;"></label>`).join("")}
|
||||
<button onclick="addPoi('${cfg.key}')">Anlegen</button>
|
||||
</div>
|
||||
<div class="msg" id="poiMsg_${cfg.key}"></div>
|
||||
</div>
|
||||
`;
|
||||
poiPanelsEl.appendChild(panel);
|
||||
});
|
||||
|
||||
function poiConfig(key) {
|
||||
return POI_CONFIGS.find(c => c.key === key);
|
||||
}
|
||||
|
||||
async function loadPoi(key) {
|
||||
const cfg = poiConfig(key);
|
||||
const res = await authFetch(cfg.endpoint);
|
||||
const data = await res.json();
|
||||
const items = data[cfg.listKey] || [];
|
||||
|
||||
const tbody = document.getElementById("poiTable_" + key);
|
||||
tbody.innerHTML = "";
|
||||
items.forEach(item => {
|
||||
const tr = document.createElement("tr");
|
||||
let priceCell = "";
|
||||
if (cfg.priceField) {
|
||||
const currentPrice = item.price !== undefined ? item.price : item.price_per_point;
|
||||
priceCell = `<td>
|
||||
<input type="number" step="0.01" value="${currentPrice}" id="poiPriceEdit_${key}_${item.id}" style="width:70px;">
|
||||
<button class="secondary" onclick="updatePoiPrice('${key}', ${item.id})" style="padding:4px 6px; font-size:11px;">✓</button>
|
||||
</td>`;
|
||||
}
|
||||
tr.innerHTML = `
|
||||
<td>#${item.id} ${escapeHtml(item.name)}</td>
|
||||
<td>${escapeHtml(item.world)}</td>
|
||||
<td>${item.x}</td>
|
||||
<td>${item.y}</td>
|
||||
${priceCell}
|
||||
<td><button class="danger" onclick="deletePoi('${key}', ${item.id})">Löschen</button></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
async function addPoi(key) {
|
||||
const cfg = poiConfig(key);
|
||||
const name = document.getElementById(`poiName_${key}`).value.trim();
|
||||
const world = document.getElementById(`poiWorld_${key}`).value.trim();
|
||||
const x = Number(document.getElementById(`poiX_${key}`).value);
|
||||
const y = Number(document.getElementById(`poiY_${key}`).value);
|
||||
|
||||
if (!name || !world || isNaN(x) || isNaN(y)) {
|
||||
showPoiMsg(key, "Name, Welt, X und Y sind Pflicht.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = { name, world, x, y };
|
||||
if (cfg.priceField) {
|
||||
payload[cfg.priceField] = Number(document.getElementById(`poiPrice_${key}`).value) || cfg.priceDefault;
|
||||
}
|
||||
(cfg.extraFields || []).forEach(f => {
|
||||
const val = document.getElementById(`poiExtra_${key}_${f.name}`).value;
|
||||
if (val !== "") payload[f.name] = f.type === "number" ? Number(val) : val;
|
||||
});
|
||||
|
||||
const res = await authFetch(cfg.endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showPoiMsg(key, "Angelegt.");
|
||||
document.getElementById(`poiName_${key}`).value = "";
|
||||
document.getElementById(`poiX_${key}`).value = "";
|
||||
document.getElementById(`poiY_${key}`).value = "";
|
||||
await loadPoi(key);
|
||||
} else {
|
||||
showPoiMsg(key, "Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePoiPrice(key, id) {
|
||||
const cfg = poiConfig(key);
|
||||
const value = Number(document.getElementById(`poiPriceEdit_${key}_${id}`).value);
|
||||
const payload = { id };
|
||||
payload[cfg.priceField] = value;
|
||||
|
||||
const res = await authFetch(cfg.endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
showPoiMsg(key, data.ok ? "Preis aktualisiert." : "Fehler beim Aktualisieren.", !data.ok);
|
||||
if (data.ok) await loadPoi(key);
|
||||
}
|
||||
|
||||
async function deletePoi(key, id) {
|
||||
if (!confirm("Diesen Eintrag wirklich löschen?")) return;
|
||||
const cfg = poiConfig(key);
|
||||
const res = await authFetch(`${cfg.endpoint}/${id}`, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
showPoiMsg(key, data.ok ? "Gelöscht." : "Löschen fehlgeschlagen.", !data.ok);
|
||||
if (data.ok) await loadPoi(key);
|
||||
}
|
||||
|
||||
function showPoiMsg(key, text, isError) {
|
||||
const el = document.getElementById("poiMsg_" + key);
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// NEWS
|
||||
// -------------------------------------------------------------
|
||||
async function loadNews() {
|
||||
const res = await fetch("/api/news");
|
||||
const data = await res.json();
|
||||
const tbody = document.getElementById("newsTableBody");
|
||||
tbody.innerHTML = "";
|
||||
(data.news || []).forEach(n => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${escapeHtml(n.title)}</td>
|
||||
<td>${escapeHtml(n.author)}</td>
|
||||
<td>${new Date(n.created_at).toLocaleString("de-DE")}</td>
|
||||
<td><button class="danger" onclick="deleteNews(${n.id})">Löschen</button></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
async function addNews() {
|
||||
const title = document.getElementById("newsTitle").value.trim();
|
||||
const content = document.getElementById("newsContent").value.trim();
|
||||
if (!title || !content) {
|
||||
showMsg("newsMsg", "Titel und Inhalt erforderlich.", true);
|
||||
return;
|
||||
}
|
||||
const res = await authFetch("/api/admin/news", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title, content })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
showMsg("newsMsg", "Veröffentlicht.");
|
||||
document.getElementById("newsTitle").value = "";
|
||||
document.getElementById("newsContent").value = "";
|
||||
await loadNews();
|
||||
} else {
|
||||
showMsg("newsMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNews(id) {
|
||||
if (!confirm("Diese News wirklich löschen?")) return;
|
||||
const res = await authFetch("/api/admin/news/" + id, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
showMsg("newsMsg", data.ok ? "Gelöscht." : "Löschen fehlgeschlagen.", !data.ok);
|
||||
if (data.ok) await loadNews();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// CHANGELOG
|
||||
// -------------------------------------------------------------
|
||||
async function loadChangelog() {
|
||||
const res = await fetch("/api/changelog");
|
||||
const data = await res.json();
|
||||
const tbody = document.getElementById("changelogTableBody");
|
||||
tbody.innerHTML = "";
|
||||
(data.changelog || []).forEach(c => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${escapeHtml(c.version)}</td>
|
||||
<td>${escapeHtml(c.title)}</td>
|
||||
<td>${new Date(c.created_at).toLocaleString("de-DE")}</td>
|
||||
<td><button class="danger" onclick="deleteChangelog(${c.id})">Löschen</button></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
async function addChangelog() {
|
||||
const version = document.getElementById("changelogVersion").value.trim();
|
||||
const title = document.getElementById("changelogTitle").value.trim();
|
||||
const content = document.getElementById("changelogContent").value.trim();
|
||||
if (!version || !title || !content) {
|
||||
showMsg("changelogMsg", "Version, Titel und Inhalt erforderlich.", true);
|
||||
return;
|
||||
}
|
||||
const res = await authFetch("/api/admin/changelog", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ version, title, content })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
showMsg("changelogMsg", "Veröffentlicht.");
|
||||
document.getElementById("changelogVersion").value = "";
|
||||
document.getElementById("changelogTitle").value = "";
|
||||
document.getElementById("changelogContent").value = "";
|
||||
await loadChangelog();
|
||||
} else {
|
||||
showMsg("changelogMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteChangelog(id) {
|
||||
if (!confirm("Diesen Changelog-Eintrag wirklich löschen?")) return;
|
||||
const res = await authFetch("/api/admin/changelog/" + id, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
showMsg("changelogMsg", data.ok ? "Gelöscht." : "Löschen fehlgeschlagen.", !data.ok);
|
||||
if (data.ok) await loadChangelog();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// DISCORD-WEBHOOKS
|
||||
// -------------------------------------------------------------
|
||||
async function loadDiscordWebhooks() {
|
||||
const res = await authFetch("/api/admin/discord_webhooks");
|
||||
const data = await res.json();
|
||||
const webhooks = data.webhooks || {};
|
||||
|
||||
["news", "changelog", "restart", "maintenance", "online_status", "tickets", "wishes", "surveys"].forEach(key => {
|
||||
const input = document.getElementById("discordUrl_" + key);
|
||||
if (input) input.value = webhooks[key] || "";
|
||||
});
|
||||
}
|
||||
|
||||
async function saveDiscordWebhook(key) {
|
||||
const url = document.getElementById("discordUrl_" + key).value.trim();
|
||||
|
||||
const res = await authFetch("/api/admin/discord_webhooks", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ key, url })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("discordMsg_" + key, url ? "Gespeichert." : "Webhook entfernt.");
|
||||
} else {
|
||||
showMsg("discordMsg_" + key, "Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// ADMIN-DIENST: AUSSEHEN
|
||||
// -------------------------------------------------------------
|
||||
async function loadAdminLookPanel() {
|
||||
const [clothingRes, skinRes, lookRes] = await Promise.all([
|
||||
authFetch("/api/admin/clothing_items"),
|
||||
authFetch("/api/skin_catalog"),
|
||||
authFetch("/api/admin/admin_look")
|
||||
]);
|
||||
const clothingData = await clothingRes.json();
|
||||
const skinData = await skinRes.json();
|
||||
const look = await lookRes.json();
|
||||
|
||||
const bySlot = { shirt: [], pants: [], shoes: [], helmet: [] };
|
||||
(clothingData.items || []).forEach(item => {
|
||||
if (bySlot[item.slot]) bySlot[item.slot].push(item);
|
||||
});
|
||||
|
||||
fillSelect("adminShirtSelect", bySlot.shirt, look.shirtId);
|
||||
fillSelect("adminPantsSelect", bySlot.pants, look.pantsId);
|
||||
fillSelect("adminShoesSelect", bySlot.shoes, look.shoesId);
|
||||
fillSelect("adminHelmetSelect", bySlot.helmet, look.helmetId);
|
||||
fillSelect("adminSkinSelect", skinData.items || [], look.skinId);
|
||||
}
|
||||
|
||||
function fillSelect(selectId, items, currentValue) {
|
||||
const select = document.getElementById(selectId);
|
||||
const placeholder = select.options[0];
|
||||
select.innerHTML = "";
|
||||
select.appendChild(placeholder);
|
||||
items.forEach(item => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = item.id;
|
||||
opt.textContent = item.name;
|
||||
if (String(item.id) === String(currentValue)) opt.selected = true;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadHighwayLinksTable() {
|
||||
const res = await authFetch("/api/admin/highway_links");
|
||||
const data = await res.json();
|
||||
const links = data.links || [];
|
||||
|
||||
const tbody = document.getElementById("highwayLinksTable");
|
||||
tbody.innerHTML = "";
|
||||
links.forEach(l => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${escapeHtml(l.name)}</td>
|
||||
<td>${escapeHtml(l.world_a)} / ${l.x_a} / ${l.y_a}</td>
|
||||
<td>${escapeHtml(l.world_b)} / ${l.x_b} / ${l.y_b}</td>
|
||||
<td><button class="secondary" onclick="deleteHighwayLink(${l.id})">Löschen</button></td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
async function addHighwayLink() {
|
||||
const name = document.getElementById("hlName").value.trim();
|
||||
const worldA = document.getElementById("hlWorldA").value.trim();
|
||||
const xA = Number(document.getElementById("hlXA").value);
|
||||
const yA = Number(document.getElementById("hlYA").value);
|
||||
const worldB = document.getElementById("hlWorldB").value.trim();
|
||||
const xB = Number(document.getElementById("hlXB").value);
|
||||
const yB = Number(document.getElementById("hlYB").value);
|
||||
|
||||
if (!worldA || !worldB || isNaN(xA) || isNaN(yA) || isNaN(xB) || isNaN(yB)) {
|
||||
showMsg("hlMsg", "Beide Welten und alle Koordinaten sind Pflicht.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await authFetch("/api/admin/highway_links", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, worldA, xA, yA, worldB, xB, yB })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.ok) {
|
||||
showMsg("hlMsg", "Verbindung angelegt.");
|
||||
await loadHighwayLinksTable();
|
||||
} else {
|
||||
showMsg("hlMsg", "Fehler: " + (data.error || "unbekannt"), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteHighwayLink(id) {
|
||||
if (!confirm("Diese Autobahn-Verbindung wirklich löschen?")) return;
|
||||
await authFetch(`/api/admin/highway_links/${id}`, { method: "DELETE" });
|
||||
await loadHighwayLinksTable();
|
||||
}
|
||||
|
||||
async function saveAdminLook() {
|
||||
const payload = {
|
||||
shirtId: document.getElementById("adminShirtSelect").value,
|
||||
pantsId: document.getElementById("adminPantsSelect").value,
|
||||
shoesId: document.getElementById("adminShoesSelect").value,
|
||||
helmetId: document.getElementById("adminHelmetSelect").value,
|
||||
skinId: document.getElementById("adminSkinSelect").value
|
||||
};
|
||||
|
||||
const res = await authFetch("/api/admin/admin_look", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
showMsg("adminLookMsg", data.ok ? "Gespeichert." : "Fehler beim Speichern.", !data.ok);
|
||||
}
|
||||
|
||||
function showMsg(elId, text, isError) {
|
||||
const el = document.getElementById(elId);
|
||||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||||
el.textContent = text;
|
||||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// TAB-NAVIGATION
|
||||
// -------------------------------------------------------------
|
||||
document.querySelectorAll(".sidebar-btn").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.querySelectorAll(".sidebar-btn").forEach(b => b.classList.remove("active"));
|
||||
document.querySelectorAll(".panel").forEach(p => p.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
const panel = document.getElementById("panel-" + btn.dataset.panel);
|
||||
if (panel) panel.classList.add("active");
|
||||
|
||||
if (btn.dataset.panel.startsWith("poi_")) {
|
||||
loadPoi(btn.dataset.panel.replace("poi_", ""));
|
||||
} else if (btn.dataset.panel === "news") {
|
||||
loadNews();
|
||||
} else if (btn.dataset.panel === "changelog") {
|
||||
loadChangelog();
|
||||
} else if (btn.dataset.panel === "discord") {
|
||||
loadDiscordWebhooks();
|
||||
} else if (btn.dataset.panel === "highway_links") {
|
||||
loadHighwayLinksTable();
|
||||
} else if (btn.dataset.panel === "adminduty") {
|
||||
loadAdminLookPanel();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Ersten Tab (erste Orts-Kategorie) initial anzeigen
|
||||
document.querySelector(".sidebar-btn").click();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user