- 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>
382 lines
43 KiB
HTML
382 lines
43 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="color-scheme" content="dark">
|
||
</head>
|
||
<body>
|
||
<title>Aether — Fraktionsverwaltung</title>
|
||
<style>
|
||
:root{
|
||
--bg:#080a0f;--fg:#e9ecf4;--muted:#828a9e;--surface:rgba(22,26,36,0.72);--surface2:rgba(34,39,52,0.85);
|
||
--slot:#111520;--border:rgba(255,255,255,0.09);--accent:#7c5cf6;--ink:#f4f0ff;--accent-soft:rgba(124,92,246,0.16);
|
||
--green:#34d39a;--amber:#f5b23d;--red:#f0506e;--blue:#4aa8ff;
|
||
}
|
||
*{box-sizing:border-box;} html,body{margin:0;height:100%;}
|
||
body{min-height:100%;background:radial-gradient(1100px 700px at 15% 0%,var(--accent-soft),transparent 55%),radial-gradient(900px 700px at 90% 100%,rgba(52,211,153,0.05),transparent 55%),var(--bg);
|
||
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:var(--fg);
|
||
display:flex;align-items:center;justify-content:center;padding:22px 14px;overflow-x:hidden;transition:background .4s;}
|
||
.app{position:relative;width:min(1280px,98vw);aspect-ratio:16/10;border-radius:20px;overflow:hidden;
|
||
background:linear-gradient(160deg,rgba(18,22,30,0.92),rgba(9,11,16,0.96));backdrop-filter:blur(20px);border:1px solid var(--border);
|
||
box-shadow:0 50px 100px -30px rgba(0,0,0,0.85);display:grid;grid-template-columns:222px 1fr;grid-template-rows:auto 1fr;grid-template-areas:"brand top" "nav main";}
|
||
.brand{grid-area:brand;padding:13px 16px;border-right:1px solid var(--border);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;}
|
||
.brand .bl{width:36px;height:36px;border-radius:10px;display:grid;place-items:center;font-weight:800;color:#fff;font-size:15px;}
|
||
.brand .bn{font-size:15px;font-weight:800;letter-spacing:-0.02em;line-height:1.1;} .brand .bn small{display:block;font-size:10px;color:var(--muted);font-weight:500;}
|
||
.top{grid-area:top;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;padding:0 18px;}
|
||
.top select{background:var(--slot);border:1px solid var(--border);color:var(--fg);border-radius:9px;padding:8px 11px;font-family:inherit;font-size:12.5px;font-weight:600;}
|
||
.top .kasse{margin-left:auto;background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:7px 13px;text-align:right;} .top .kasse .l{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;font-weight:800;} .top .kasse .v{font-size:15px;font-weight:800;font-variant-numeric:tabular-nums;color:var(--green);}
|
||
.top .me{display:flex;align-items:center;gap:9px;padding-left:14px;border-left:1px solid var(--border);} .top .me .av{width:32px;height:32px;border-radius:50%;display:grid;place-items:center;font-weight:800;font-size:12px;color:#fff;} .top .me .mn{font-size:13px;font-weight:700;} .top .me .mr{font-size:10.5px;color:var(--accent);font-weight:700;}
|
||
.nav{grid-area:nav;border-right:1px solid var(--border);padding:11px 9px;display:flex;flex-direction:column;gap:2px;overflow-y:auto;}
|
||
.nav button{display:flex;align-items:center;gap:10px;padding:9px 11px;border-radius:9px;border:none;background:none;color:var(--muted);font-family:inherit;font-size:13.5px;font-weight:700;cursor:pointer;text-align:left;transition:.12s;}
|
||
.nav button .ni{font-size:15px;width:18px;text-align:center;} .nav button:hover{background:var(--surface);color:var(--fg);} .nav button.active{background:var(--accent-soft);color:var(--accent);}
|
||
.nav button .badge{margin-left:auto;background:var(--slot);color:var(--muted);font-size:10px;font-weight:800;border-radius:999px;padding:1px 7px;}
|
||
.main{grid-area:main;overflow-y:auto;padding:18px 22px;} .main::-webkit-scrollbar,.nav::-webkit-scrollbar,.tblwrap::-webkit-scrollbar{width:7px;} .main::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.09);border-radius:4px;}
|
||
.phead{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin-bottom:14px;} .phead h2{font-size:20px;margin:0;font-weight:800;letter-spacing:-0.02em;} .phead p{margin:2px 0 0;color:var(--muted);font-size:12.5px;}
|
||
|
||
.grid{display:grid;gap:12px;} .card{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:15px;}
|
||
.kpi .l{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;font-weight:800;} .kpi .v{font-size:24px;font-weight:800;font-variant-numeric:tabular-nums;margin-top:3px;} .kpi .d{font-size:11.5px;font-weight:600;margin-top:1px;}
|
||
.list-h{font-size:10.5px;text-transform:uppercase;letter-spacing:0.06em;color:var(--muted);font-weight:800;margin:0 0 10px;display:flex;align-items:center;gap:8px;} .list-h .r{margin-left:auto;font-weight:600;text-transform:none;letter-spacing:0;}
|
||
.btn{border:none;border-radius:10px;padding:10px 15px;font-size:13px;font-weight:700;cursor:pointer;font-family:inherit;transition:.12s;} .btn.primary{background:var(--accent);color:#fff;} .btn.primary:hover{filter:brightness(1.1);} .btn.ghost{background:var(--surface2);color:var(--fg);border:1px solid var(--border);} .btn.red{background:var(--red);color:#2a0910;} .btn.green{background:var(--green);color:#06231a;} .btn.sm{padding:6px 11px;font-size:12px;} .btn:disabled{opacity:.4;cursor:not-allowed;} .btn:active{transform:scale(0.98);}
|
||
.input,select.input{width:100%;background:var(--slot);border:1px solid var(--border);border-radius:9px;padding:10px 11px;color:var(--fg);font-size:13.5px;font-family:inherit;outline:none;} .input:focus{border-color:var(--accent);}
|
||
.flabel{font-size:10.5px;color:var(--muted);font-weight:800;text-transform:uppercase;letter-spacing:0.04em;margin:11px 2px 5px;display:block;}
|
||
.row{display:flex;align-items:center;gap:11px;padding:10px 12px;background:var(--surface);border:1px solid var(--border);border-radius:12px;margin-bottom:7px;} .row .grow{flex:1;min-width:0;} .row .t{font-weight:700;font-size:13.5px;} .row .m{font-size:11.5px;color:var(--muted);}
|
||
.av{border-radius:50%;display:grid;place-items:center;font-weight:800;color:#fff;flex-shrink:0;width:36px;height:36px;font-size:12px;}
|
||
.pill{font-size:10px;font-weight:800;padding:3px 8px;border-radius:999px;display:inline-block;} .pill.on{background:rgba(52,211,153,0.16);color:var(--green);} .pill.off{background:rgba(255,255,255,0.08);color:var(--muted);} .pill.a{background:rgba(245,178,61,0.16);color:var(--amber);} .pill.r{background:rgba(240,80,110,0.16);color:var(--red);} .pill.ac{background:var(--accent-soft);color:var(--accent);}
|
||
.tblwrap{overflow-x:auto;} table.tbl{width:100%;border-collapse:collapse;font-size:12.5px;} .tbl th{text-align:left;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:0.05em;padding:8px 9px;border-bottom:1px solid var(--border);white-space:nowrap;} .tbl td{padding:9px 9px;border-bottom:1px solid var(--border);} .tbl tr:last-child td{border:none;} .tbl .num{text-align:right;font-variant-numeric:tabular-nums;} .tbl .c{text-align:center;}
|
||
.toggle{width:34px;height:20px;border-radius:999px;background:var(--slot);position:relative;cursor:pointer;border:1px solid var(--border);display:inline-block;vertical-align:middle;transition:.15s;} .toggle::after{content:"";position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;transition:.15s;} .toggle.on{background:var(--accent);border-color:transparent;} .toggle.on::after{transform:translateX(14px);} .toggle.dis{opacity:.4;cursor:not-allowed;}
|
||
.empty{text-align:center;color:var(--muted);padding:36px 10px;} .empty .big{font-size:36px;margin-bottom:8px;}
|
||
.bartrack{height:8px;border-radius:5px;background:var(--slot);overflow:hidden;} .bartrack>i{display:block;height:100%;border-radius:5px;}
|
||
|
||
/* Territorien-Karte */
|
||
.tmap{position:relative;border-radius:14px;overflow:hidden;border:1px solid var(--border);height:340px;
|
||
background:repeating-linear-gradient(90deg,rgba(255,255,255,0.02) 0 1px,transparent 1px 56px),repeating-linear-gradient(0deg,rgba(255,255,255,0.02) 0 1px,transparent 1px 56px),linear-gradient(135deg,#101823,#0a0f16);}
|
||
.zone{position:absolute;border-radius:10px;border:1.5px solid;cursor:pointer;display:flex;align-items:flex-end;padding:7px;transition:.12s;} .zone:hover{filter:brightness(1.15);}
|
||
.zone .zn{font-size:11px;font-weight:800;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,0.7);} .zone .zc{position:absolute;top:6px;right:8px;font-size:10px;font-weight:800;color:#fff;text-shadow:0 1px 2px #000;}
|
||
|
||
.overlay{position:absolute;inset:0;z-index:40;background:rgba(4,6,10,0.72);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;padding:22px;} .overlay.show{display:flex;}
|
||
.modal{width:460px;max-width:97%;max-height:90%;overflow-y:auto;background:rgba(18,22,30,0.98);border:1px solid var(--border);border-radius:16px;padding:20px;box-shadow:0 30px 70px -18px rgba(0,0,0,0.8);} .modal.sm{width:380px;}
|
||
.modal h3{margin:0 0 3px;font-size:18px;} .modal .msub{color:var(--muted);font-size:12.5px;margin-bottom:12px;} .modal-actions{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-top:14px;}
|
||
.kv{display:flex;justify-content:space-between;font-size:12.5px;padding:5px 0;border-bottom:1px solid var(--border);} .kv:last-child{border:none;} .kv span:first-child{color:var(--muted);}
|
||
.toast{position:absolute;bottom:16px;left:50%;transform:translateX(-50%) translateY(16px);background:rgba(12,15,22,0.97);border:1px solid var(--border);color:var(--fg);padding:10px 18px;border-radius:11px;font-size:13px;font-weight:600;z-index:50;opacity:0;pointer-events:none;transition:.3s;} .toast.show{opacity:1;transform:translateX(-50%) translateY(0);} .toast.good{border-color:var(--green);} .toast.warn{border-color:var(--red);}
|
||
</style>
|
||
|
||
<div class="app">
|
||
<div class="brand"><span class="bl" id="brand-logo">B</span><div class="bn" id="brand-name">Fraktion<small>Verwaltung</small></div></div>
|
||
<div class="top" id="top"></div>
|
||
<div class="nav" id="nav"></div>
|
||
<div class="main" id="main"></div>
|
||
<div class="overlay" id="overlay"></div>
|
||
<div class="toast" id="toast"></div>
|
||
</div>
|
||
|
||
<script>
|
||
"use strict";
|
||
const $=id=>document.getElementById(id);
|
||
function sicher(t){const d=document.createElement("div");d.textContent=t==null?"":t;return d.innerHTML;}
|
||
const euro=n=>Math.round(n).toLocaleString("de-DE")+" €";
|
||
const initialen=n=>n.split(" ").map(w=>w[0]).slice(0,2).join("");
|
||
const avColor=n=>{let h=0;for(const c of n)h=(h*31+c.charCodeAt(0))%360;return `hsl(${h} 45% 46%)`;};
|
||
let toastTimer=null; function toast(t,typ){const e=$("toast");e.textContent=t;e.className="toast show"+(typ?" "+typ:"");clearTimeout(toastTimer);toastTimer=setTimeout(()=>e.classList.remove("show"),2400);}
|
||
function modal(html,sm){$("overlay").innerHTML=`<div class="modal${sm?' sm':''}">${html}</div>`;$("overlay").classList.add("show");}
|
||
function closeModal(){$("overlay").classList.remove("show");$("overlay").innerHTML="";}
|
||
|
||
/* =====================================================================
|
||
RECHTE-DEFINITIONEN
|
||
===================================================================== */
|
||
const RECHTE=[["einladen","Einladen"],["kicken","Kicken"],["befoerdern","Befördern"],["kasse","Kasse"],["raenge","Ränge"],["territorien","Territorien"],["fahrzeuge","Fahrzeuge"],["einstellungen","Einstell."]];
|
||
const alleRechte=(v)=>Object.fromEntries(RECHTE.map(r=>[r[0],v]));
|
||
function rang(name,gehalt,rechte){return {name,gehalt,rechte:{...alleRechte(false),...rechte}};}
|
||
const spieler={bargeld:12500}; // eigenes Bargeld (für Kasse-Einzahlung)
|
||
|
||
/* =====================================================================
|
||
FRAKTIONEN
|
||
===================================================================== */
|
||
const fraktionen=[
|
||
{id:"ballas",name:"Ballas",kurz:"Straßengang · Grove/Davis",typ:"gang",farbe:"#7c5cf6",ink:"#f4f0ff",kasse:184000,
|
||
raenge:[rang("Boss",0,alleRechte(true)),rang("Underboss",4000,{einladen:1,kicken:1,befoerdern:1,kasse:1,territorien:1,fahrzeuge:1}),rang("Enforcer",2600,{einladen:1,kicken:1,territorien:1,fahrzeuge:1}),rang("Soldier",1500,{fahrzeuge:1}),rang("Prospect",600,{})],
|
||
mitglieder:[{name:"Deebo King",rankIdx:0,seit:"12.01.2026",letzte:"online",online:true},{name:"Vince Malloy",rankIdx:1,seit:"02.02.2026",letzte:"online",online:true},{name:"Rico Vargas",rankIdx:2,seit:"14.03.2026",letzte:"online",online:true},{name:"Tyrone Banks",rankIdx:3,seit:"20.04.2026",letzte:"vor 2 h",online:false},{name:"Marcus Hale",rankIdx:3,seit:"01.05.2026",letzte:"online",online:true},{name:"Lil J",rankIdx:4,seit:"18.06.2026",letzte:"vor 1 Tag",online:false}],
|
||
territorien:[
|
||
{name:"Grove Street",x:14,y:60,w:24,h:26,kontrolle:88,einnahmen:320,rivale:"Families"},
|
||
{name:"Davis",x:42,y:64,w:22,h:24,kontrolle:64,einnahmen:210,rivale:"Vagos"},
|
||
{name:"Chamberlain Hills",x:20,y:26,w:26,h:24,kontrolle:41,einnahmen:150,rivale:"Vagos"},
|
||
{name:"Strawberry",x:56,y:30,w:24,h:22,kontrolle:12,einnahmen:0,rivale:"Vagos"}],
|
||
fahrzeuge:[{model:"Manana",plate:"BAL-01",zugewiesen:"Enforcer+"},{model:"Baller",plate:"BAL-02",zugewiesen:"Boss"}],
|
||
logs:[], transaktionen:[{typ:"in",text:"Territorium Grove Street",betrag:320,zeit:"heute"},{typ:"out",text:"Waffenkauf",betrag:-8000,zeit:"gestern"}]},
|
||
{id:"vagos",name:"Vagos",kurz:"Straßengang · East LS",typ:"gang",farbe:"#f5b23d",ink:"#3a2600",kasse:97000,
|
||
raenge:[rang("Boss",0,alleRechte(true)),rang("Underboss",3800,{einladen:1,kicken:1,befoerdern:1,kasse:1,territorien:1,fahrzeuge:1}),rang("Veterano",2400,{einladen:1,kicken:1,territorien:1}),rang("Soldado",1400,{fahrzeuge:1}),rang("Novato",500,{})],
|
||
mitglieder:[{name:"Miguel Cruz",rankIdx:0,seit:"08.01.2026",letzte:"online",online:true},{name:"Ana Reyes",rankIdx:1,seit:"11.02.2026",letzte:"vor 30 min",online:false},{name:"Diego Sol",rankIdx:2,seit:"22.03.2026",letzte:"online",online:true},{name:"Pablo Nieto",rankIdx:3,seit:"14.05.2026",letzte:"online",online:true}],
|
||
territorien:[
|
||
{name:"El Rancho",x:60,y:56,w:26,h:26,kontrolle:82,einnahmen:280,rivale:"Ballas"},
|
||
{name:"Cypress Flats",x:60,y:24,w:24,h:24,kontrolle:58,einnahmen:190,rivale:"—"},
|
||
{name:"Davis",x:36,y:62,w:22,h:22,kontrolle:36,einnahmen:110,rivale:"Ballas"}],
|
||
fahrzeuge:[{model:"Buccaneer",plate:"VAG-01",zugewiesen:"alle"}],
|
||
logs:[], transaktionen:[{typ:"in",text:"Schutzgeld East LS",betrag:1500,zeit:"heute"}]},
|
||
{id:"lsc",name:"Los Santos Customs",kurz:"Legaler Betrieb · Werkstatt",typ:"legal",farbe:"#34d39a",ink:"#06231a",kasse:342000,
|
||
raenge:[rang("Inhaber",0,alleRechte(true)),rang("Werkstattmeister",4200,{einladen:1,kicken:1,befoerdern:1,kasse:1,fahrzeuge:1}),rang("Geselle",2400,{fahrzeuge:1}),rang("Lehrling",1200,{})],
|
||
mitglieder:[{name:"Karl Mendez",rankIdx:0,seit:"03.11.2025",letzte:"online",online:true},{name:"Ben Fischer",rankIdx:1,seit:"20.12.2025",letzte:"online",online:true},{name:"Sara Lind",rankIdx:2,seit:"14.02.2026",letzte:"vor 5 h",online:false},{name:"Tom Kessler",rankIdx:3,seit:"30.04.2026",letzte:"online",online:true}],
|
||
territorien:[],
|
||
fahrzeuge:[{model:"Flatbed",plate:"LSC-01",zugewiesen:"Geselle+"},{model:"Towtruck",plate:"LSC-02",zugewiesen:"alle"},{model:"Sultan RS",plate:"LSC-03",zugewiesen:"Meister+"}],
|
||
logs:[], transaktionen:[{typ:"in",text:"Reparaturaufträge",betrag:12400,zeit:"heute"},{typ:"out",text:"Ersatzteile",betrag:-3200,zeit:"gestern"}]}
|
||
];
|
||
let aktiv=0; // aktive Fraktion
|
||
let ansichtRang=0; // "angemeldet als" Rang (zum Testen der Rechte); 0 = Boss/Inhaber
|
||
let sektion="uebersicht";
|
||
|
||
const F=()=>fraktionen[aktiv];
|
||
const R=()=>F().raenge[ansichtRang];
|
||
const darf=recht=>!!R().rechte[recht];
|
||
const rangName=idx=>F().raenge[idx].name;
|
||
function log(aktion,detail){F().logs.unshift({zeit:new Date(),wer:F().mitglieder.find(m=>m.rankIdx===ansichtRang)?.name||rangName(ansichtRang),aktion,detail:detail||""});}
|
||
|
||
/* =====================================================================
|
||
SHELL
|
||
===================================================================== */
|
||
const nav=[
|
||
{id:"uebersicht",label:"Übersicht",icon:"🏠"},
|
||
{id:"mitglieder",label:"Mitglieder",icon:"👥"},
|
||
{id:"raenge",label:"Ränge & Rechte",icon:"🎖️"},
|
||
{id:"kasse",label:"Kasse",icon:"💰"},
|
||
{id:"territorien",label:"Territorien",icon:"🗺️",gang:true},
|
||
{id:"fahrzeuge",label:"Fahrzeuge",icon:"🚗"},
|
||
{id:"logbuch",label:"Logbuch",icon:"📜"},
|
||
{id:"einstellungen",label:"Einstellungen",icon:"⚙️"}
|
||
];
|
||
function starte(){
|
||
document.documentElement.style.setProperty("--accent",F().farbe);
|
||
document.documentElement.style.setProperty("--ink",F().ink);
|
||
document.documentElement.style.setProperty("--accent-soft","color-mix(in srgb, "+F().farbe+" 16%, transparent)");
|
||
$("brand-logo").style.background=F().farbe; $("brand-logo").textContent=initialen(F().name);
|
||
$("brand-name").innerHTML=`${sicher(F().name)}<small>${F().typ==="gang"?"Gang-Verwaltung":"Fraktions-Verwaltung"}</small>`;
|
||
renderTop(); renderNav(); renderSektion();
|
||
}
|
||
function renderTop(){
|
||
$("top").innerHTML=`<select id="frak-sel">${fraktionen.map((f,i)=>`<option value="${i}" ${i===aktiv?'selected':''}>${sicher(f.name)}</option>`).join('')}</select>
|
||
<select id="rang-sel" title="Ansicht als Rang (Rechte testen)">${F().raenge.map((r,i)=>`<option value="${i}" ${i===ansichtRang?'selected':''}>Als: ${sicher(r.name)}</option>`).join('')}</select>
|
||
<div class="kasse"><div class="l">Fraktionskasse</div><div class="v">${euro(F().kasse)}</div></div>
|
||
<div class="me"><span class="av" style="background:${F().farbe}">${initialen(rangName(ansichtRang))}</span><div><div class="mn">${sicher(rangName(ansichtRang))}</div><div class="mr">${F().mitglieder.filter(m=>m.online).length} online</div></div></div>`;
|
||
$("frak-sel").addEventListener("change",e=>{aktiv=+e.target.value;ansichtRang=0;sektion="uebersicht";starte();});
|
||
$("rang-sel").addEventListener("change",e=>{ansichtRang=+e.target.value;starte();});
|
||
}
|
||
function renderNav(){
|
||
$("nav").innerHTML=nav.filter(n=>!n.gang||F().typ==="gang").map(n=>`<button data-s="${n.id}" class="${n.id===sektion?'active':''}"><span class="ni">${n.icon}</span>${n.label}${n.id==='mitglieder'?`<span class="badge">${F().mitglieder.length}</span>`:''}</button>`).join('');
|
||
$("nav").querySelectorAll("[data-s]").forEach(b=>b.addEventListener("click",()=>{sektion=b.dataset.s;renderNav();renderSektion();}));
|
||
}
|
||
function renderSektion(){(sektionen[sektion]||sektionen.uebersicht)();}
|
||
const sektionen={};
|
||
|
||
/* =====================================================================
|
||
ÜBERSICHT
|
||
===================================================================== */
|
||
sektionen.uebersicht=()=>{
|
||
const online=F().mitglieder.filter(m=>m.online).length, payday=F().mitglieder.reduce((s,m)=>s+F().raenge[m.rankIdx].gehalt,0);
|
||
const terrIncome=F().territorien.reduce((s,t)=>s+(t.kontrolle>=50?t.einnahmen:0),0);
|
||
$("main").innerHTML=`<div class="phead"><div><h2>${sicher(F().name)}</h2><p>${sicher(F().kurz)}</p></div></div>
|
||
<div class="grid" style="grid-template-columns:repeat(4,1fr)">
|
||
<div class="card kpi"><div class="l">👥 Mitglieder</div><div class="v">${F().mitglieder.length}</div><div class="d" style="color:var(--green)">${online} online</div></div>
|
||
<div class="card kpi"><div class="l">💰 Kasse</div><div class="v">${euro(F().kasse)}</div><div class="d" style="color:var(--muted)">Fraktionsvermögen</div></div>
|
||
<div class="card kpi"><div class="l">🧾 Payday /h</div><div class="v">${euro(payday)}</div><div class="d" style="color:var(--muted)">Gehälter gesamt</div></div>
|
||
<div class="card kpi"><div class="l">${F().typ==='gang'?'🗺️ Gebiets-Einnahmen':'🔧 Aufträge /h'}</div><div class="v" style="color:var(--green)">${F().typ==='gang'?euro(terrIncome)+' /h':euro(8400)+' /h'}</div><div class="d" style="color:var(--muted)">${F().typ==='gang'?F().territorien.filter(t=>t.kontrolle>=50).length+' kontrollierte Gebiete':'geschätzt'}</div></div></div>
|
||
<div class="grid" style="grid-template-columns:1fr 1fr;margin-top:12px">
|
||
<div class="card"><div class="list-h">Online-Mitglieder</div>${F().mitglieder.filter(m=>m.online).map(m=>`<div class="row" style="margin:0 0 7px"><span class="av" style="background:${avColor(m.name)}">${initialen(m.name)}</span><div class="grow"><div class="t">${sicher(m.name)}</div><div class="m">${rangName(m.rankIdx)}</div></div><span class="pill on">online</span></div>`).join('')||'<div class="empty">Niemand online.</div>'}</div>
|
||
<div class="card"><div class="list-h">Schnellaktionen</div><div style="display:flex;flex-direction:column;gap:8px">
|
||
<button class="btn ghost" id="q-invite" ${darf('einladen')?'':'disabled'}>➕ Mitglied einladen</button>
|
||
<button class="btn ghost" id="q-payday" ${darf('kasse')?'':'disabled'}>🧾 Payday auszahlen (${euro(payday)})</button>
|
||
<button class="btn ghost" id="q-deposit" ${darf('kasse')?'':'disabled'}>💵 In Kasse einzahlen</button>
|
||
${F().typ==='gang'?'<button class="btn ghost" id="q-terr">🗺️ Territorien verwalten</button>':''}</div>
|
||
${darf('einladen')?'':'<div style="font-size:11px;color:var(--muted);margin-top:8px">🔒 Als '+sicher(rangName(ansichtRang))+' hast du eingeschränkte Rechte.</div>'}</div></div>`;
|
||
const qi=$("q-invite");if(qi)qi.addEventListener("click",()=>darf('einladen')&&einladenModal());
|
||
const qp=$("q-payday");if(qp)qp.addEventListener("click",()=>darf('kasse')&&payday());
|
||
const qd=$("q-deposit");if(qd)qd.addEventListener("click",()=>darf('kasse')&&kasseModal('in'));
|
||
const qt=$("q-terr");if(qt)qt.addEventListener("click",()=>{sektion="territorien";renderNav();renderSektion();});
|
||
};
|
||
|
||
/* =====================================================================
|
||
MITGLIEDER
|
||
===================================================================== */
|
||
sektionen.mitglieder=()=>{
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Mitglieder</h2><p>${F().mitglieder.length} Mitglieder · ${F().mitglieder.filter(m=>m.online).length} online</p></div>
|
||
<button class="btn primary" id="m-invite" ${darf('einladen')?'':'disabled'}>➕ Einladen</button></div>
|
||
<div class="card" style="padding:6px 12px"><div class="tblwrap"><table class="tbl"><thead><tr><th>Mitglied</th><th>Rang</th><th class="num">Gehalt</th><th>Seit</th><th>Status</th><th></th></tr></thead><tbody>
|
||
${F().mitglieder.map((m,i)=>`<tr><td><span class="av" style="width:26px;height:26px;font-size:10px;background:${avColor(m.name)}">${initialen(m.name)}</span> ${sicher(m.name)}</td>
|
||
<td><span class="pill ac">${rangName(m.rankIdx)}</span></td><td class="num">${euro(F().raenge[m.rankIdx].gehalt)}</td><td style="color:var(--muted)">${m.seit}</td>
|
||
<td>${m.online?'<span class="pill on">online</span>':`<span class="pill off">${sicher(m.letzte)}</span>`}</td>
|
||
<td class="num"><button class="btn ghost sm" data-up="${i}" ${kannBefoerdern(m,1)?'':'disabled'}>▲</button> <button class="btn ghost sm" data-down="${i}" ${kannBefoerdern(m,-1)?'':'disabled'}>▼</button> <button class="btn red sm" data-kick="${i}" ${kannKicken(m)?'':'disabled'}>Kick</button></td></tr>`).join('')}
|
||
</tbody></table></div></div>
|
||
${darf('befoerdern')||darf('kicken')?'':'<div class="card" style="margin-top:10px;color:var(--muted);font-size:12px">🔒 Als '+sicher(rangName(ansichtRang))+' kannst du keine Ränge ändern oder Mitglieder entfernen.</div>'}`;
|
||
const mi=$("m-invite");if(mi)mi.addEventListener("click",()=>darf('einladen')&&einladenModal());
|
||
$("main").querySelectorAll("[data-up]").forEach(b=>b.addEventListener("click",()=>befoerdern(+b.dataset.up,1)));
|
||
$("main").querySelectorAll("[data-down]").forEach(b=>b.addEventListener("click",()=>befoerdern(+b.dataset.down,-1)));
|
||
$("main").querySelectorAll("[data-kick]").forEach(b=>b.addEventListener("click",()=>kickMitglied(+b.dataset.kick)));
|
||
};
|
||
function kannBefoerdern(m,dir){ if(!darf('befoerdern'))return false; const neu=m.rankIdx-dir; // dir +1 = hoch (kleinerer Index)
|
||
if(neu<0||neu>=F().raenge.length)return false; if(neu<=ansichtRang&&ansichtRang!==0)return false; // nicht auf/über eigenen Rang befördern (außer Boss)
|
||
if(m.rankIdx<=ansichtRang&&ansichtRang!==0)return false; return true; }
|
||
function kannKicken(m){ if(!darf('kicken'))return false; return m.rankIdx>ansichtRang||ansichtRang===0&&m.rankIdx!==0; }
|
||
function befoerdern(i,dir){const m=F().mitglieder[i];if(!kannBefoerdern(m,dir))return;m.rankIdx-=dir;log(dir>0?"Befördert":"Degradiert",m.name+" → "+rangName(m.rankIdx));toast(m.name+" "+(dir>0?"befördert":"degradiert")+" zu "+rangName(m.rankIdx),"good");renderSektion();renderTop();}
|
||
function kickMitglied(i){const m=F().mitglieder[i];if(!kannKicken(m))return;
|
||
modal(`<h3>Mitglied entfernen?</h3><p style="color:var(--muted);font-size:13px">„${sicher(m.name)}“ (${rangName(m.rankIdx)}) aus ${sicher(F().name)} entfernen?</p><div class="modal-actions"><button class="btn ghost" id="k-c">Abbrechen</button><button class="btn red" id="k-ok">Entfernen</button></div>`,true);
|
||
$("k-c").addEventListener("click",closeModal);$("k-ok").addEventListener("click",()=>{F().mitglieder.splice(i,1);log("Mitglied entfernt",m.name);closeModal();toast(m.name+" entfernt","warn");renderSektion();renderTop();renderNav();});}
|
||
function einladenModal(){
|
||
modal(`<h3>Mitglied einladen</h3><div class="msub">Neues Mitglied zu ${sicher(F().name)} hinzufügen</div>
|
||
<label class="flabel">Name / ID</label><input class="input" id="ei-name" placeholder="z. B. Max Weber">
|
||
<label class="flabel">Startrang</label><select class="input" id="ei-rang">${F().raenge.map((r,i)=>i>0?`<option value="${i}" ${i===F().raenge.length-1?'selected':''}>${sicher(r.name)}</option>`:'').join('')}</select>
|
||
<div class="modal-actions"><button class="btn ghost" id="ei-c">Abbrechen</button><button class="btn primary" id="ei-ok">Einladen</button></div>`,true);
|
||
$("ei-c").addEventListener("click",closeModal);
|
||
$("ei-ok").addEventListener("click",()=>{const n=$("ei-name").value.trim();if(!n){toast("Name angeben","warn");return;}F().mitglieder.push({name:n,rankIdx:+$("ei-rang").value,seit:new Date().toLocaleDateString('de-DE'),letzte:"gerade beigetreten",online:true});log("Mitglied eingeladen",n);closeModal();toast(n+" eingeladen","good");renderSektion();renderTop();renderNav();});
|
||
}
|
||
|
||
/* =====================================================================
|
||
RÄNGE & RECHTE
|
||
===================================================================== */
|
||
sektionen.raenge=()=>{
|
||
const editier=darf('raenge');
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Ränge & Rechte</h2><p>${F().raenge.length} Ränge · Gehälter & Berechtigungen</p></div>${editier?'<button class="btn primary" id="r-neu">➕ Rang</button>':''}</div>
|
||
<div class="card" style="padding:6px 12px"><div class="tblwrap"><table class="tbl"><thead><tr><th>Rang</th><th class="num">Gehalt /h</th>${RECHTE.map(r=>`<th class="c">${r[1]}</th>`).join('')}<th></th></tr></thead><tbody>
|
||
${F().raenge.map((r,i)=>`<tr><td><span class="pill ac">${sicher(r.name)}</span>${i===0?' <span class="pill a">Leitung</span>':''}</td>
|
||
<td class="num">${i===0?'—':euro(r.gehalt)}</td>
|
||
${RECHTE.map(rc=>`<td class="c"><span class="toggle ${r.rechte[rc[0]]?'on':''} ${editier&&i!==0?'':'dis'}" data-rt="${i}|${rc[0]}"></span></td>`).join('')}
|
||
<td class="num">${editier&&i!==0?`<button class="btn ghost sm" data-gehalt="${i}">Gehalt</button> <button class="btn red sm" data-delr="${i}" ${F().mitglieder.some(m=>m.rankIdx===i)?'disabled title="Rang belegt"':''}>✕</button>`:''}</td></tr>`).join('')}
|
||
</tbody></table></div></div>
|
||
<div class="card" style="margin-top:10px;font-size:12px;color:var(--muted)">Der oberste Rang (Leitung) besitzt automatisch alle Rechte. ${editier?'Klicke die Schalter, um Rechte je Rang zu ändern.':'🔒 Nur mit dem Recht „Ränge" bearbeitbar.'}</div>`;
|
||
if(editier){
|
||
const rn=$("r-neu");if(rn)rn.addEventListener("click",neuerRangModal);
|
||
$("main").querySelectorAll("[data-rt]").forEach(t=>t.addEventListener("click",()=>{const [i,rc]=t.dataset.rt.split('|');if(+i===0)return;const r=F().raenge[+i];r.rechte[rc]=!r.rechte[rc];log("Recht geändert",F().raenge[+i].name+": "+rc+" "+(r.rechte[rc]?'an':'aus'));renderSektion();}));
|
||
$("main").querySelectorAll("[data-gehalt]").forEach(b=>b.addEventListener("click",()=>{const r=F().raenge[+b.dataset.gehalt];
|
||
modal(`<h3>Gehalt · ${sicher(r.name)}</h3><label class="flabel">Gehalt (€ / Stunde Payday)</label><input class="input" id="rg" type="number" value="${r.gehalt}"><div class="modal-actions"><button class="btn ghost" id="rg-c">Abbrechen</button><button class="btn primary" id="rg-ok">Speichern</button></div>`,true);
|
||
$("rg-c").addEventListener("click",closeModal);$("rg-ok").addEventListener("click",()=>{r.gehalt=Math.max(0,parseInt($("rg").value,10)||0);log("Gehalt geändert",r.name+" → "+euro(r.gehalt));closeModal();toast("Gehalt aktualisiert","good");renderSektion();});}));
|
||
$("main").querySelectorAll("[data-delr]").forEach(b=>b.addEventListener("click",()=>{const i=+b.dataset.delr;if(F().mitglieder.some(m=>m.rankIdx===i)){toast("Rang ist belegt","warn");return;}const r=F().raenge.splice(i,1)[0];F().mitglieder.forEach(m=>{if(m.rankIdx>i)m.rankIdx--;});log("Rang gelöscht",r.name);toast("Rang gelöscht","warn");renderSektion();}));
|
||
}
|
||
};
|
||
function neuerRangModal(){
|
||
modal(`<h3>Neuer Rang</h3><label class="flabel">Name</label><input class="input" id="nr-name" placeholder="z. B. Veteran"><label class="flabel">Gehalt (€ / h)</label><input class="input" id="nr-gehalt" type="number" value="1000">
|
||
<div class="modal-actions"><button class="btn ghost" id="nr-c">Abbrechen</button><button class="btn primary" id="nr-ok">Erstellen</button></div>`,true);
|
||
$("nr-c").addEventListener("click",closeModal);
|
||
$("nr-ok").addEventListener("click",()=>{const n=$("nr-name").value.trim();if(!n){toast("Name angeben","warn");return;}F().raenge.push(rang(n,parseInt($("nr-gehalt").value,10)||0,{}));log("Rang erstellt",n);closeModal();toast("Rang „"+n+"“ erstellt","good");renderSektion();});
|
||
}
|
||
|
||
/* =====================================================================
|
||
KASSE
|
||
===================================================================== */
|
||
sektionen.kasse=()=>{
|
||
const kann=darf('kasse'), payday=F().mitglieder.reduce((s,m)=>s+F().raenge[m.rankIdx].gehalt,0);
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Fraktionskasse</h2><p>Ein- und Auszahlungen · Payday</p></div></div>
|
||
<div class="grid" style="grid-template-columns:repeat(3,1fr);margin-bottom:12px">
|
||
<div class="card kpi"><div class="l">Kassenstand</div><div class="v" style="color:var(--green)">${euro(F().kasse)}</div></div>
|
||
<div class="card kpi"><div class="l">Payday /h</div><div class="v">${euro(payday)}</div><div class="d" style="color:var(--muted)">alle Gehälter</div></div>
|
||
<div class="card kpi"><div class="l">Dein Bargeld</div><div class="v">${euro(spieler.bargeld)}</div></div></div>
|
||
<div class="grid" style="grid-template-columns:1fr 1.4fr;align-items:start">
|
||
<div class="card"><div class="list-h">Aktionen</div><div style="display:flex;flex-direction:column;gap:8px">
|
||
<button class="btn ghost" id="k-in" ${kann?'':'disabled'}>💵 Einzahlen</button>
|
||
<button class="btn ghost" id="k-out" ${kann?'':'disabled'}>🏧 Auszahlen</button>
|
||
<button class="btn primary" id="k-pay" ${kann?'':'disabled'}>🧾 Payday auszahlen (${euro(payday)})</button></div>
|
||
${kann?'':'<div style="font-size:11px;color:var(--muted);margin-top:8px">🔒 Kein Recht „Kasse".</div>'}</div>
|
||
<div class="card"><div class="list-h">Transaktionen</div>${F().transaktionen.map(t=>`<div class="kv"><span>${t.typ==='in'?'⬆️':'⬇️'} ${sicher(t.text)}<span style="color:var(--muted)"> · ${t.zeit}</span></span><span style="color:${t.betrag>0?'var(--green)':'var(--red)'};font-weight:700">${t.betrag>0?'+':''}${euro(t.betrag)}</span></div>`).join('')||'<div class="empty">Keine Transaktionen.</div>'}</div></div>`;
|
||
const ki=$("k-in");if(ki)ki.addEventListener("click",()=>kann&&kasseModal('in'));
|
||
const ko=$("k-out");if(ko)ko.addEventListener("click",()=>kann&&kasseModal('out'));
|
||
const kp=$("k-pay");if(kp)kp.addEventListener("click",()=>kann&&payday());
|
||
};
|
||
function kasseModal(dir){
|
||
const rein=dir==='in';
|
||
modal(`<h3>${rein?'In Kasse einzahlen':'Aus Kasse auszahlen'}</h3><div class="msub">${rein?'Von deinem Bargeld ('+euro(spieler.bargeld)+')':'Auf dein Bargeld · Kasse: '+euro(F().kasse)}</div>
|
||
<label class="flabel">Betrag (€)</label><input class="input" id="ka-betrag" type="number" placeholder="0">
|
||
<label class="flabel">Verwendungszweck</label><input class="input" id="ka-zweck" placeholder="optional">
|
||
<div class="modal-actions"><button class="btn ghost" id="ka-c">Abbrechen</button><button class="btn primary" id="ka-ok">${rein?'Einzahlen':'Auszahlen'}</button></div>`,true);
|
||
$("ka-c").addEventListener("click",closeModal);
|
||
$("ka-ok").addEventListener("click",()=>{const b=parseInt($("ka-betrag").value,10);if(!b||b<=0){toast("Betrag angeben","warn");return;}
|
||
if(rein){if(b>spieler.bargeld){toast("Nicht genug Bargeld","warn");return;}spieler.bargeld-=b;F().kasse+=b;F().transaktionen.unshift({typ:'in',text:$("ka-zweck").value.trim()||"Einzahlung ("+rangName(ansichtRang)+")",betrag:b,zeit:"gerade eben"});log("Kasse eingezahlt",euro(b));toast(euro(b)+" eingezahlt","good");}
|
||
else{if(b>F().kasse){toast("Kasse nicht gedeckt","warn");return;}F().kasse-=b;spieler.bargeld+=b;F().transaktionen.unshift({typ:'out',text:$("ka-zweck").value.trim()||"Auszahlung ("+rangName(ansichtRang)+")",betrag:-b,zeit:"gerade eben"});log("Kasse ausgezahlt",euro(b));toast(euro(b)+" ausgezahlt","good");}
|
||
closeModal();renderSektion();renderTop();});
|
||
}
|
||
function payday(){const gesamt=F().mitglieder.reduce((s,m)=>s+F().raenge[m.rankIdx].gehalt,0);
|
||
if(gesamt>F().kasse){toast("Kasse deckt den Payday nicht ("+euro(gesamt)+")","warn");return;}
|
||
F().kasse-=gesamt;F().transaktionen.unshift({typ:'out',text:"Payday ("+F().mitglieder.length+" Mitglieder)",betrag:-gesamt,zeit:"gerade eben"});log("Payday ausgezahlt",euro(gesamt));toast("Payday "+euro(gesamt)+" ausgezahlt","good");renderSektion();renderTop();}
|
||
|
||
/* =====================================================================
|
||
TERRITORIEN (nur Gangs)
|
||
===================================================================== */
|
||
function terrStatus(k){return k>=66?["kontrolliert","on",F().farbe]:k>=33?["umkämpft","a","#f5b23d"]:["feindlich","r","#f0506e"];}
|
||
sektionen.territorien=()=>{
|
||
if(F().typ!=="gang"){$("main").innerHTML='<div class="empty" style="padding:60px"><div class="big">🏢</div>Territorien gibt es nur für Gangs.</div>';return;}
|
||
const kontrolliert=F().territorien.filter(t=>t.kontrolle>=50).length, income=F().territorien.reduce((s,t)=>s+(t.kontrolle>=50?t.einnahmen:0),0);
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Territorien</h2><p>${kontrolliert}/${F().territorien.length} kontrolliert · ${euro(income)} /h Einnahmen</p></div></div>
|
||
<div style="display:grid;grid-template-columns:1fr 300px;gap:12px;align-items:start">
|
||
<div class="tmap" id="tmap">${F().territorien.map((t,i)=>{const st=terrStatus(t.kontrolle);return `<div class="zone" data-z="${i}" style="left:${t.x}%;top:${t.y}%;width:${t.w}%;height:${t.h}%;background:${st[2]}22;border-color:${st[2]}"><span class="zc">${t.kontrolle}%</span><span class="zn">${sicher(t.name)}</span></div>`;}).join('')}</div>
|
||
<div><div class="list-h">Gebiete</div>${F().territorien.map((t,i)=>{const st=terrStatus(t.kontrolle);return `<div class="card" style="padding:12px;margin-bottom:8px;cursor:pointer" data-z="${i}"><div style="display:flex;align-items:center;justify-content:space-between"><b>${sicher(t.name)}</b><span class="pill ${st[1]}">${st[0]}</span></div>
|
||
<div class="bartrack" style="margin:8px 0 5px"><i style="width:${t.kontrolle}%;background:${st[2]}"></i></div>
|
||
<div style="font-size:11.5px;color:var(--muted)">Kontrolle ${t.kontrolle}% · ${t.kontrolle>=50?euro(t.einnahmen)+' /h':'keine Einnahmen'} · Rivale: ${sicher(t.rivale)}</div></div>`;}).join('')}</div></div>`;
|
||
$("main").querySelectorAll("[data-z]").forEach(el=>el.addEventListener("click",()=>terrModal(+el.dataset.z)));
|
||
};
|
||
function terrModal(i){const t=F().territorien[i],st=terrStatus(t.kontrolle),kann=darf('territorien');
|
||
modal(`<h3>${sicher(t.name)}</h3><div class="msub">Rivale: ${sicher(t.rivale)}</div>
|
||
<div class="kv"><span>Kontrolle</span><span><span class="pill ${st[1]}">${st[0]}</span> ${t.kontrolle}%</span></div>
|
||
<div class="kv"><span>Einnahmen</span><span>${t.kontrolle>=50?euro(t.einnahmen)+' /h':'—'}</span></div>
|
||
<div class="bartrack" style="margin:10px 0"><i style="width:${t.kontrolle}%;background:${st[2]}"></i></div>
|
||
${kann?`<div class="modal-actions" style="grid-template-columns:1fr 1fr"><button class="btn primary" id="t-attack">⚔️ Angreifen</button><button class="btn ghost" id="t-defend">🛡️ Verstärken</button></div>
|
||
<div class="modal-actions" style="grid-template-columns:1fr 1fr;margin-top:8px"><button class="btn green" id="t-upgrade" ${t.kontrolle>=50?'':'disabled'}>⬆️ Ausbauen · ${euro(6000)}</button><button class="btn ghost" id="t-close">Schließen</button></div>`
|
||
:`<div style="font-size:12px;color:var(--muted);margin-top:8px">🔒 Kein Recht „Territorien".</div><div class="modal-actions" style="grid-template-columns:1fr;margin-top:8px"><button class="btn ghost" id="t-close">Schließen</button></div>`}`);
|
||
$("t-close").addEventListener("click",closeModal);
|
||
if(kann){
|
||
$("t-attack").addEventListener("click",()=>{t.kontrolle=Math.min(100,t.kontrolle+10+Math.floor(Math.random()*15));log("Angriff",t.name+" → "+t.kontrolle+"%");toast("Angriff auf "+t.name+" · Kontrolle "+t.kontrolle+"%","good");renderSektion();closeModal();});
|
||
$("t-defend").addEventListener("click",()=>{t.kontrolle=Math.min(100,t.kontrolle+5);log("Verstärkt",t.name);toast(t.name+" verstärkt · "+t.kontrolle+"%");renderSektion();closeModal();});
|
||
const up=$("t-upgrade");if(up)up.addEventListener("click",()=>{if(F().kasse<6000){toast("Kasse nicht gedeckt","warn");return;}F().kasse-=6000;t.einnahmen+=80;F().transaktionen.unshift({typ:'out',text:"Ausbau "+t.name,betrag:-6000,zeit:"gerade eben"});log("Gebiet ausgebaut",t.name+" +80/h");toast(t.name+" ausgebaut · +80 €/h","good");renderSektion();renderTop();closeModal();});
|
||
}
|
||
}
|
||
|
||
/* =====================================================================
|
||
FAHRZEUGE
|
||
===================================================================== */
|
||
sektionen.fahrzeuge=()=>{
|
||
const kann=darf('fahrzeuge');
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Fraktionsfahrzeuge</h2><p>${F().fahrzeuge.length} Fahrzeuge · gemeinsame Garage</p></div>${kann?'<button class="btn primary" id="fz-add">➕ Hinzufügen</button>':''}</div>
|
||
<div class="card" style="padding:6px 12px"><div class="tblwrap"><table class="tbl"><thead><tr><th>Modell</th><th>Kennzeichen</th><th>Freigabe ab</th><th></th></tr></thead><tbody>
|
||
${F().fahrzeuge.map((v,i)=>`<tr><td>🚗 ${sicher(v.model)}</td><td class="mono">${sicher(v.plate)}</td><td>${sicher(v.zugewiesen)}</td><td class="num">${kann?`<button class="btn red sm" data-delv="${i}">Entfernen</button>`:''}</td></tr>`).join('')||'<tr><td colspan="4" class="empty">Keine Fahrzeuge.</td></tr>'}
|
||
</tbody></table></div></div>`;
|
||
const fa=$("fz-add");if(fa)fa.addEventListener("click",()=>{
|
||
modal(`<h3>Fahrzeug hinzufügen</h3><label class="flabel">Modell</label><input class="input" id="fv-model" placeholder="z. B. Sultan"><label class="flabel">Kennzeichen</label><input class="input" id="fv-plate" placeholder="z. B. GNG-04"><label class="flabel">Freigabe ab Rang</label><select class="input" id="fv-rang"><option value="alle">alle Mitglieder</option>${F().raenge.map(r=>`<option>${sicher(r.name)}+</option>`).join('')}</select><div class="modal-actions"><button class="btn ghost" id="fv-c">Abbrechen</button><button class="btn primary" id="fv-ok">Hinzufügen</button></div>`,true);
|
||
$("fv-c").addEventListener("click",closeModal);$("fv-ok").addEventListener("click",()=>{const m=$("fv-model").value.trim();if(!m){toast("Modell angeben","warn");return;}F().fahrzeuge.push({model:m,plate:$("fv-plate").value.trim()||"GNG-"+Math.floor(10+Math.random()*90),zugewiesen:$("fv-rang").value});log("Fahrzeug hinzugefügt",m);closeModal();toast(m+" hinzugefügt","good");renderSektion();});});
|
||
$("main").querySelectorAll("[data-delv]").forEach(b=>b.addEventListener("click",()=>{const v=F().fahrzeuge.splice(+b.dataset.delv,1)[0];log("Fahrzeug entfernt",v.plate);toast(v.model+" entfernt","warn");renderSektion();}));
|
||
};
|
||
|
||
/* =====================================================================
|
||
LOGBUCH
|
||
===================================================================== */
|
||
sektionen.logbuch=()=>{
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Logbuch</h2><p>${F().logs.length} protokollierte Aktionen</p></div></div>
|
||
<div class="card" style="padding:6px 12px"><div class="tblwrap"><table class="tbl"><thead><tr><th>Zeit</th><th>Von</th><th>Aktion</th><th>Detail</th></tr></thead><tbody>
|
||
${F().logs.map(l=>`<tr><td class="mono" style="color:var(--muted)">${l.zeit.toLocaleTimeString('de-DE')}</td><td>${sicher(l.wer)}</td><td>${sicher(l.aktion)}</td><td style="color:var(--muted)">${sicher(l.detail)}</td></tr>`).join('')||'<tr><td colspan="4" class="empty">Noch keine Einträge.</td></tr>'}
|
||
</tbody></table></div></div>`;
|
||
};
|
||
|
||
/* =====================================================================
|
||
EINSTELLUNGEN
|
||
===================================================================== */
|
||
sektionen.einstellungen=()=>{
|
||
const kann=darf('einstellungen');
|
||
$("main").innerHTML=`<div class="phead"><div><h2>Einstellungen</h2><p>Fraktionsdaten & Rekrutierung</p></div></div>
|
||
<div class="grid" style="grid-template-columns:1fr 1fr;align-items:start">
|
||
<div class="card"><div class="list-h">Fraktion</div>
|
||
<label class="flabel">Name</label><input class="input" id="s-name" value="${sicher(F().name)}" ${kann?'':'disabled'}>
|
||
<label class="flabel">Beschreibung</label><input class="input" id="s-desc" value="${sicher(F().kurz)}" ${kann?'':'disabled'}>
|
||
<label class="flabel">Farbe</label><div style="display:flex;gap:8px;flex-wrap:wrap" id="s-farbe">${["#7c5cf6","#f5b23d","#34d39a","#4aa8ff","#f0506e","#e0503f","#22c1d4","#c96fd6"].map(c=>`<div data-c="${c}" style="width:30px;height:30px;border-radius:8px;background:${c};cursor:${kann?'pointer':'default'};border:2px solid ${F().farbe===c?'#fff':'transparent'}"></div>`).join('')}</div>
|
||
${kann?'<button class="btn primary" id="s-save" style="margin-top:12px">Speichern</button>':'<div style="font-size:11px;color:var(--muted);margin-top:10px">🔒 Kein Recht „Einstellungen".</div>'}</div>
|
||
<div class="card"><div class="list-h">Typ & Status</div>
|
||
<div class="kv"><span>Typ</span><span>${F().typ==='gang'?'Gang (illegal)':'Legale Fraktion'}</span></div>
|
||
<div class="kv"><span>Mitglieder</span><span>${F().mitglieder.length}</span></div>
|
||
<div class="kv"><span>Ränge</span><span>${F().raenge.length}</span></div>
|
||
<div class="kv"><span>Kasse</span><span>${euro(F().kasse)}</span></div>
|
||
${F().typ==='gang'?`<div class="kv"><span>Territorien</span><span>${F().territorien.length}</span></div>`:''}</div></div>`;
|
||
if(kann){
|
||
let farbe=F().farbe;
|
||
$("s-farbe").querySelectorAll("[data-c]").forEach(el=>el.addEventListener("click",()=>{farbe=el.dataset.c;$("s-farbe").querySelectorAll("[data-c]").forEach(x=>x.style.borderColor='transparent');el.style.borderColor='#fff';}));
|
||
$("s-save").addEventListener("click",()=>{F().name=$("s-name").value.trim()||F().name;F().kurz=$("s-desc").value.trim();F().farbe=farbe;log("Einstellungen geändert",F().name);toast("Gespeichert","good");starte();});
|
||
}
|
||
};
|
||
|
||
/* START */
|
||
starte();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|