- 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>
331 lines
25 KiB
HTML
331 lines
25 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 — Garage</title>
|
|
<style>
|
|
:root{
|
|
--bg:#080a0f;--fg:#eaf0f8;--muted:#828da3;--surface:rgba(22,26,36,0.72);--surface2:rgba(36,41,55,0.85);
|
|
--slot:#11141c;--border:rgba(255,255,255,0.09);--accent:#3aa0ff;--ink:#04182f;--accent-soft:rgba(58,160,255,0.15);
|
|
--red:#f0506e;--green:#34d39a;--amber:#f5b23d;
|
|
}
|
|
*{box-sizing:border-box;} html,body{margin:0;height:100%;}
|
|
body{min-height:100%;background:radial-gradient(1100px 700px at 20% 0%,rgba(58,160,255,0.09),transparent 55%),radial-gradient(900px 700px at 90% 100%,rgba(52,211,153,0.06),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:26px 16px;overflow-x:hidden;}
|
|
.garage{position:relative;width:min(1200px,97vw);aspect-ratio:16/10;border-radius:22px;overflow:hidden;
|
|
background:linear-gradient(160deg,rgba(20,23,32,0.92),rgba(10,12,17,0.96));backdrop-filter:blur(20px);
|
|
border:1px solid var(--border);box-shadow:0 50px 100px -30px rgba(0,0,0,0.85);display:flex;flex-direction:column;}
|
|
.g-head{display:flex;align-items:center;gap:14px;padding:16px 24px;border-bottom:1px solid var(--border);}
|
|
.g-head .logo{width:40px;height:40px;border-radius:11px;display:grid;place-items:center;font-size:20px;background:linear-gradient(150deg,var(--accent),#1a5fa8);box-shadow:0 8px 20px var(--accent-soft);}
|
|
.g-head h1{font-size:19px;margin:0;font-weight:800;letter-spacing:-0.02em;} .g-head h1 small{display:block;font-size:11.5px;color:var(--muted);font-weight:500;}
|
|
.g-head select{margin-left:auto;background:var(--slot);border:1px solid var(--border);color:var(--fg);border-radius:10px;padding:9px 12px;font-family:inherit;font-size:13px;font-weight:600;}
|
|
.g-head .count{font-size:12.5px;color:var(--muted);font-weight:600;}
|
|
|
|
.g-body{flex:1;display:grid;grid-template-columns:300px 1fr;min-height:0;}
|
|
.veh-list{border-right:1px solid var(--border);padding:14px;overflow-y:auto;display:flex;flex-direction:column;gap:9px;}
|
|
.veh-list::-webkit-scrollbar{width:7px;} .veh-list::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.09);border-radius:4px;}
|
|
.veh-card{display:flex;align-items:center;gap:12px;padding:11px;border-radius:14px;background:var(--surface);border:1px solid var(--border);cursor:pointer;transition:.14s;}
|
|
.veh-card:hover{border-color:var(--accent);transform:translateX(2px);} .veh-card.active{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent) inset;background:var(--accent-soft);}
|
|
.veh-card .thumb{width:66px;height:40px;flex-shrink:0;display:grid;place-items:center;} .veh-card .thumb svg{width:100%;height:100%;}
|
|
.veh-card .vn{font-size:14px;font-weight:800;} .veh-card .vm{font-size:11.5px;color:var(--muted);}
|
|
.veh-card .vp{margin-top:2px;}
|
|
|
|
.veh-detail{position:relative;display:grid;grid-template-rows:1fr auto;overflow:hidden;}
|
|
.stage{position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden;
|
|
background:radial-gradient(60% 55% at 50% 55%,rgba(58,160,255,0.08),transparent 70%),repeating-linear-gradient(0deg,rgba(255,255,255,0.015) 0 2px,transparent 2px 46px);}
|
|
.stage .floor{position:absolute;bottom:14%;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.14),transparent);}
|
|
.stage .bigcar{width:78%;max-width:640px;position:relative;z-index:1;filter:drop-shadow(0 26px 30px rgba(0,0,0,0.5));}
|
|
.stage .vname{position:absolute;top:22px;left:28px;} .stage .vname h2{font-size:26px;margin:0;font-weight:800;letter-spacing:-0.02em;} .stage .vname .cls{color:var(--accent);font-weight:700;font-size:13px;}
|
|
.stage .statuspill{position:absolute;top:26px;right:28px;}
|
|
|
|
.detail-foot{border-top:1px solid var(--border);padding:16px 24px;display:grid;grid-template-columns:1.1fr 1fr;gap:22px;align-items:center;}
|
|
.cond{display:flex;flex-direction:column;gap:9px;}
|
|
.cbar{display:flex;align-items:center;gap:10px;} .cbar .cl{width:88px;font-size:12px;color:var(--muted);font-weight:600;} .cbar .ct{flex:1;height:9px;border-radius:5px;background:var(--slot);overflow:hidden;} .cbar .cf{height:100%;border-radius:5px;} .cbar .cv{width:42px;text-align:right;font-size:12px;font-weight:700;font-variant-numeric:tabular-nums;}
|
|
.plate-wrap{display:flex;flex-direction:column;align-items:center;gap:10px;}
|
|
.plate-note{font-size:10.5px;color:var(--muted);font-weight:600;}
|
|
.ausp-hint{font-size:12px;color:var(--amber);font-weight:700;align-self:center;}
|
|
.actions{display:flex;gap:9px;align-items:center;grid-column:1/-1;}
|
|
.btn{border:none;border-radius:11px;padding:12px 20px;font-size:14px;font-weight:800;cursor:pointer;font-family:inherit;transition:.12s;}
|
|
.btn.primary{background:var(--accent);color:var(--ink);} .btn.primary:hover{filter:brightness(1.08);} .btn.ghost{background:var(--surface2);color:var(--fg);border:1px solid var(--border);} .btn.amber{background:var(--amber);color:#3a2600;} .btn:active{transform:scale(0.98);}
|
|
.pill{font-size:11px;font-weight:800;padding:4px 11px;border-radius:999px;} .pill.in{background:rgba(52,211,153,0.16);color:var(--green);} .pill.out{background:rgba(245,178,61,0.16);color:var(--amber);} .pill.imp{background:rgba(240,80,110,0.16);color:var(--red);}
|
|
|
|
/* ============ Kennzeichen (San Andreas) ============ */
|
|
.plate{width:230px;aspect-ratio:2.1/1;border-radius:11px;position:relative;padding:6px;
|
|
box-shadow:0 8px 20px -6px rgba(0,0,0,0.6),0 0 0 1px rgba(0,0,0,0.3);}
|
|
.plate .pin{width:100%;height:100%;border-radius:7px;display:flex;flex-direction:column;align-items:center;justify-content:center;border:2px solid;position:relative;overflow:hidden;}
|
|
.plate .ptop{font-size:11px;font-weight:800;letter-spacing:0.14em;line-height:1;margin-top:2px;}
|
|
.plate .pnum{font-family:"Arial Narrow","Helvetica Neue",Arial,sans-serif;font-weight:800;font-size:38px;letter-spacing:0.06em;line-height:1;margin-top:2px;}
|
|
.plate .screw{position:absolute;width:7px;height:7px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#dfe2e6,#8b9099);box-shadow:0 1px 1px rgba(0,0,0,0.4);}
|
|
.plate .screw.tl{top:9px;left:9px;} .plate .screw.tr{top:9px;right:9px;} .plate .screw.bl{bottom:9px;left:9px;} .plate .screw.br{bottom:9px;right:9px;}
|
|
/* Standard San Andreas: blau auf creme */
|
|
.plate.sa{background:linear-gradient(180deg,#fbfbf5,#e9e9dd);} .plate.sa .pin{border-color:#2a3f7a;background:linear-gradient(180deg,#fdfdf7,#eeeee2);}
|
|
.plate.sa .ptop{color:#2a3f7a;} .plate.sa .pnum{color:#243a72;text-shadow:0 1px 0 rgba(255,255,255,0.6);}
|
|
/* Gelb */
|
|
.plate.yellow{background:linear-gradient(180deg,#ffd23f,#f2b71e);} .plate.yellow .pin{border-color:#1c2a12;background:linear-gradient(180deg,#ffd84f,#f4bd22);}
|
|
.plate.yellow .ptop{color:#3a2e08;} .plate.yellow .pnum{color:#20180a;text-shadow:0 1px 0 rgba(255,255,255,0.35);}
|
|
/* Schwarz (gelb auf schwarz) */
|
|
.plate.black{background:linear-gradient(180deg,#26282c,#141517);} .plate.black .pin{border-color:#e8c860;background:linear-gradient(180deg,#222327,#141517);}
|
|
.plate.black .ptop{color:#e8c860;} .plate.black .pnum{color:#f0d878;text-shadow:0 1px 2px rgba(0,0,0,0.6);}
|
|
.plate.mini{width:96px;padding:3px;box-shadow:none;} .plate.mini .pin{border-width:1.5px;} .plate.mini .ptop{font-size:5.5px;letter-spacing:0.1em;} .plate.mini .pnum{font-size:15px;} .plate.mini .screw{display:none;}
|
|
|
|
.toast{position:absolute;bottom:18px;left:50%;transform:translateX(-50%) translateY(16px);background:rgba(12,15,22,0.97);border:1px solid var(--border);color:var(--fg);padding:11px 20px;border-radius:12px;font-size:13.5px;font-weight:600;z-index:40;opacity:0;pointer-events:none;transition:.3s;} .toast.show{opacity:1;transform:translateX(-50%) translateY(0);} .toast.warn{border-color:var(--red);} .toast.good{border-color:var(--green);}
|
|
</style>
|
|
|
|
<div class="garage">
|
|
<div class="g-head"><span class="logo">🚗</span><div><h1>Garage<small id="g-loc">Legion Square</small></h1></div>
|
|
<select id="g-select"></select><span class="count" id="g-count" style="margin-left:14px"></span></div>
|
|
<div class="g-body">
|
|
<div class="veh-list" id="veh-list"></div>
|
|
<div class="veh-detail">
|
|
<div class="stage"><div class="floor"></div><div class="vname" id="v-name"></div><span class="statuspill" id="v-status"></span><div class="bigcar" id="v-car"></div></div>
|
|
<div class="detail-foot" id="v-foot"></div>
|
|
</div>
|
|
</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;}
|
|
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 schattiere(hex,f){const n=parseInt(hex.slice(1),16);let r=(n>>16)&255,g=(n>>8)&255,b=n&255;r=Math.max(0,Math.min(255,Math.round(r*f)));g=Math.max(0,Math.min(255,Math.round(g*f)));b=Math.max(0,Math.min(255,Math.round(b*f)));return "#"+((1<<24)+(r<<16)+(g<<8)+b).toString(16).slice(1);}
|
|
|
|
/* =====================================================================
|
|
FAHRZEUGE
|
|
===================================================================== */
|
|
/* Garagen: öffentlich sichtbar für alle; Haus-Garagen nur mit Zugang (Besitz/berechtigt).
|
|
Nicht zugängliche Garagen (zugang:false) erscheinen NICHT in der Auswahl. */
|
|
/* Garagen und Fahrzeuge kommen vom Server. Der Server schickt nur
|
|
Garagen, zu denen der Spieler wirklich Zugang hat. */
|
|
let garagen=[
|
|
{id:"legion", name:"Legion Square (öffentlich)", typ:"öffentlich"},
|
|
{id:"sandy", name:"Sandy Shores Garage (öffentlich)", typ:"öffentlich"},
|
|
{id:"delperro", name:"Del Perro Tiefgarage (öffentlich)",typ:"öffentlich"},
|
|
{id:"villa", name:"Villa Vinewood Hills (dein Haus)", typ:"haus", zugang:true},
|
|
{id:"apartment",name:"Apartment 4B (Zugang)", typ:"haus", zugang:true},
|
|
{id:"privat", name:"Privatgarage Mirror Park", typ:"haus", zugang:false} // kein Zugang -> unsichtbar
|
|
];
|
|
let fahrzeuge=[
|
|
{name:"Sultan RS",klasse:"Sportwagen",typ:"sport",farbe:"#c0392b",kennzeichen:"46EEK572",plateStyle:"sa",tank:78,motor:92,karosserie:85,km:14230,garage:"legion",status:"eingelagert"},
|
|
{name:"Bati 801",klasse:"Motorrad",typ:"bike",farbe:"#1c1c1e",kennzeichen:"88KLM214",plateStyle:"black",tank:64,motor:100,karosserie:96,km:5120,garage:"legion",status:"eingelagert"},
|
|
{name:"Bison",klasse:"Pickup",typ:"pickup",farbe:"#2f6b4a",kennzeichen:"21PRT903",plateStyle:"sa",tank:41,motor:76,karosserie:63,km:38900,garage:"sandy",status:"draußen"},
|
|
{name:"Baller",klasse:"SUV",typ:"suv",farbe:"#20232a",kennzeichen:"77VBN018",plateStyle:"sa",tank:88,motor:98,karosserie:91,km:9800,garage:"delperro",status:"eingelagert"},
|
|
{name:"Issi",klasse:"Kleinwagen",typ:"compact",farbe:"#3563a8",kennzeichen:"09HGT456",plateStyle:"yellow",tank:52,motor:83,karosserie:88,km:22140,garage:"legion",status:"beschlagnahmt"},
|
|
{name:"Dominator",klasse:"Muscle",typ:"muscle",farbe:"#e0a020",kennzeichen:"33MSC120",plateStyle:"black",tank:70,motor:89,karosserie:79,km:16540,garage:"villa",status:"eingelagert"}
|
|
];
|
|
// Zugängliche Garagen (öffentlich oder mit Zugang)
|
|
const zugaenglich=()=>garagen.filter(g=>g.typ==="öffentlich"||g.zugang===true);
|
|
const garageName=id=>{const g=garagen.find(x=>x.id===id);return g?g.name:id;};
|
|
let standort="legion"; // vom Server gesetzt // aktueller physischer Standort des Spielers
|
|
let aktiveGarage="legion"; // aktuell angezeigte Garage ("alle" = Übersicht)
|
|
let auswahl=0; // Index in gefilterter Liste
|
|
|
|
/* =====================================================================
|
|
FAHRZEUG-VORSCHAU (Seitenansicht, parametrisch nach Typ)
|
|
===================================================================== */
|
|
// Rad mit Reifen und Felge
|
|
function rad(cx,cy,r){let s=`<circle cx="${cx}" cy="${cy}" r="${r}" fill="#14161b"/><circle cx="${cx}" cy="${cy}" r="${(r*0.58).toFixed(1)}" fill="#2a2e37"/><circle cx="${cx}" cy="${cy}" r="${(r*0.5).toFixed(1)}" fill="none" stroke="#5a616e" stroke-width="2"/>`;
|
|
for(let k=0;k<6;k++){const a=k/6*6.283;s+=`<line x1="${cx}" y1="${cy}" x2="${(cx+Math.cos(a)*r*0.5).toFixed(1)}" y2="${(cy+Math.sin(a)*r*0.5).toFixed(1)}" stroke="#5a616e" stroke-width="2.4"/>`;}
|
|
return s+`<circle cx="${cx}" cy="${cy}" r="4" fill="#6b7280"/>`;
|
|
}
|
|
// Karosserie-Formen je Typ
|
|
const carShapes={
|
|
sport: {body:"M56 150 C52 126 74 120 104 118 L152 92 C182 78 250 78 288 98 L338 118 C366 122 376 132 376 150 C376 158 372 162 362 162 L70 162 C60 162 56 158 56 150 Z",win:"M164 100 C186 87 246 87 274 100 L296 116 L150 116 Z",wheels:[[116,154,27],[304,154,27]],head:[366,132],tail:[56,134]},
|
|
sedan: {body:"M54 148 C50 122 70 116 100 114 L140 96 C150 80 175 74 212 74 C252 74 272 84 284 98 L340 116 C368 120 378 130 378 150 C378 158 374 162 364 162 L68 162 C58 162 54 156 54 148 Z",win:"M148 98 C158 82 182 78 212 78 C248 78 266 86 278 98 L300 114 L146 114 Z",wheels:[[118,154,26],[302,154,26]],head:[368,130],tail:[56,132]},
|
|
suv: {body:"M52 146 C50 118 66 110 96 108 L120 78 C126 66 152 60 212 60 C282 60 300 68 308 82 L332 108 C366 112 380 122 380 150 C380 158 376 162 366 162 L66 162 C56 162 52 156 52 146 Z",win:"M126 82 C132 68 156 64 212 64 C272 64 292 70 300 84 L314 106 L120 106 Z",wheels:[[116,156,29],[300,156,29]],head:[370,128],tail:[54,130]},
|
|
pickup: {body:"M52 148 C50 122 66 114 92 112 L112 84 C118 72 142 68 178 68 C208 68 222 76 228 88 L238 112 L360 112 C374 114 382 124 382 150 C382 158 378 162 368 162 L66 162 C56 162 52 156 52 148 Z",win:"M118 88 C124 74 144 72 178 72 C204 72 216 78 222 90 L228 110 L114 110 Z",wheels:[[112,156,27],[320,156,27]],head:[372,130],tail:[54,132],bed:[240,238,112]},
|
|
compact:{body:"M76 148 C72 124 92 118 118 116 L150 94 C162 80 186 76 216 76 C248 76 264 84 274 98 L316 116 C340 120 348 130 348 150 C348 158 344 162 336 162 L90 162 C80 162 76 156 76 148 Z",win:"M154 98 C164 84 188 80 216 80 C246 80 260 88 270 100 L292 116 L152 116 Z",wheels:[[132,154,24],[300,154,24]],head:[340,132],tail:[76,134]},
|
|
muscle: {body:"M50 150 C46 124 68 118 100 116 L156 94 C186 80 252 80 286 98 L344 118 C372 122 384 132 384 150 C384 158 380 162 370 162 L64 162 C54 162 50 158 50 150 Z",win:"M168 100 C190 88 248 88 276 100 L298 116 L156 116 Z",wheels:[[116,154,28],[308,154,28]],head:[374,134],tail:[50,136]}
|
|
};
|
|
function carSvg(typ,farbe){
|
|
const fL=schattiere(farbe,1.24),fD=schattiere(farbe,0.68),uid="cg"+Math.random().toString(36).slice(2,7);
|
|
if(typ==="bike")return bikeSvg(farbe,fL,fD,uid);
|
|
const s=carShapes[typ]||carShapes.sedan;
|
|
return `<svg viewBox="0 0 420 200"><defs><linearGradient id="${uid}" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="${fL}"/><stop offset="52%" stop-color="${farbe}"/><stop offset="100%" stop-color="${fD}"/></linearGradient></defs>
|
|
<ellipse cx="212" cy="180" rx="160" ry="10" fill="rgba(0,0,0,0.32)"/>
|
|
<path d="${s.body}" fill="url(#${uid})" stroke="${fD}" stroke-width="1"/>
|
|
${s.bed?`<path d="M ${s.bed[0]} 112 L ${s.bed[0]} 130 L ${s.bed[1]} 130 L ${s.bed[1]} 112" fill="none" stroke="${fD}" stroke-width="3"/>`:""}
|
|
<path d="${s.win}" fill="#111a26" opacity="0.92"/><path d="${s.win}" fill="none" stroke="${schattiere(farbe,0.9)}" stroke-width="2"/>
|
|
<rect x="${s.head[0]}" y="${s.head[1]}" width="9" height="12" rx="2" fill="#ffe08a"/>
|
|
<rect x="${s.tail[0]}" y="${s.tail[1]}" width="8" height="12" rx="2" fill="#e0503f"/>
|
|
<path d="M ${s.wheels[0][0]+20} 162 L ${s.wheels[1][0]-20} 162" stroke="${fD}" stroke-width="1" opacity="0.5"/>
|
|
${s.wheels.map(w=>rad(w[0],w[1],w[2])).join("")}
|
|
</svg>`;
|
|
}
|
|
function bikeSvg(farbe,fL,fD,uid){
|
|
return `<svg viewBox="0 0 420 200">
|
|
<ellipse cx="212" cy="182" rx="140" ry="9" fill="rgba(0,0,0,0.3)"/>
|
|
<path d="M132 150 L200 112 L252 110 L300 150" stroke="#2a2e37" stroke-width="9" fill="none" stroke-linecap="round"/>
|
|
<path d="M118 150 L150 130" stroke="#5a616e" stroke-width="6" stroke-linecap="round"/>
|
|
<path d="M300 112 L308 150" stroke="#5a616e" stroke-width="7" stroke-linecap="round"/>
|
|
<path d="M170 120 C186 104 224 104 236 118 L240 130 L162 132 Z" fill="${farbe}" stroke="${fD}" stroke-width="1"/>
|
|
<path d="M150 132 L182 126 L184 136 L150 140 Z" fill="#18181a"/>
|
|
<path d="M288 100 C312 104 320 128 306 148 L292 142 C302 128 298 112 282 108 Z" fill="${farbe}" stroke="${fD}" stroke-width="1"/>
|
|
<circle cx="314" cy="120" r="6" fill="#ffe08a"/>
|
|
<path d="M300 104 L322 96" stroke="#2a2e37" stroke-width="5" stroke-linecap="round"/>
|
|
${rad(118,150,34)}${rad(304,150,34)}
|
|
</svg>`;
|
|
}
|
|
|
|
/* =====================================================================
|
|
KENNZEICHEN (San Andreas) — verschiedene Stile
|
|
===================================================================== */
|
|
function plateHtml(text,style,mini){
|
|
return `<div class="plate ${style}${mini?' mini':''}"><div class="pin"><div class="ptop">SAN ANDREAS</div><div class="pnum">${sicher(text)}</div></div>
|
|
<span class="screw tl"></span><span class="screw tr"></span><span class="screw bl"></span><span class="screw br"></span></div>`;
|
|
}
|
|
|
|
/* =====================================================================
|
|
RENDERN
|
|
===================================================================== */
|
|
// nur zugängliche Garagen; "alle" = Übersicht aller zugänglichen Fahrzeuge
|
|
function gefilterte(){
|
|
const ids=zugaenglich().map(g=>g.id);
|
|
return fahrzeuge.filter(v=>ids.includes(v.garage)&&(aktiveGarage==="alle"||v.garage===aktiveGarage));
|
|
}
|
|
function renderGarageSelect(){
|
|
const opts=zugaenglich().map(g=>`<option value="${g.id}" ${g.id===aktiveGarage?'selected':''}>${g.id===standort?'📍 ':''}${sicher(g.name)}</option>`).join("");
|
|
$("g-select").innerHTML=`<option value="alle" ${aktiveGarage==="alle"?'selected':''}>Alle (Übersicht)</option>`+opts;
|
|
$("g-select").value=aktiveGarage;
|
|
$("g-select").onchange=()=>{aktiveGarage=$("g-select").value;auswahl=0;render();};
|
|
}
|
|
function render(){
|
|
renderGarageSelect();
|
|
const liste=gefilterte();
|
|
$("g-loc").textContent="📍 Du bist an: "+garageName(standort);
|
|
$("g-count").textContent=liste.length+" Fahrzeug"+(liste.length===1?"":"e")+(aktiveGarage==="alle"?" · alle Garagen":"");
|
|
auswahl=Math.max(0,Math.min(auswahl,liste.length-1));
|
|
const lc=$("veh-list");
|
|
lc.innerHTML=liste.map((v,i)=>`<div class="veh-card ${i===auswahl?'active':''}" data-i="${i}">
|
|
<div class="thumb">${carSvg(v.typ,v.farbe)}</div>
|
|
<div class="grow"><div class="vn">${sicher(v.name)} ${v.garage===standort?'<span class="pill in" style="font-size:9px;padding:2px 6px">hier</span>':''}</div>
|
|
<div class="vm">${sicher(v.klasse)}${aktiveGarage==="alle"?' · '+garageName(v.garage).replace(/\s*\(.*\)/,''):''}</div>
|
|
<div class="vp">${plateHtml(v.kennzeichen,v.plateStyle,true)}</div></div></div>`).join("")||'<div style="color:var(--muted);text-align:center;padding:30px">Keine Fahrzeuge in dieser Garage.</div>';
|
|
lc.querySelectorAll("[data-i]").forEach(c=>c.addEventListener("click",()=>{auswahl=+c.dataset.i;render();}));
|
|
renderDetail(liste[auswahl]);
|
|
}
|
|
const statusInfo={eingelagert:["Eingelagert","in"],"draußen":["Ausgeparkt","out"],beschlagnahmt:["Beschlagnahmt","imp"]};
|
|
function condColor(v){return v>=80?"var(--green)":v>=50?"var(--amber)":"var(--red)";}
|
|
function renderDetail(v){
|
|
if(!v){$("v-name").innerHTML="";$("v-car").innerHTML="";$("v-status").innerHTML="";$("v-foot").innerHTML='<div style="color:var(--muted)">Kein Fahrzeug ausgewählt.</div>';return;}
|
|
const st=statusInfo[v.status], amStandort=v.garage===standort;
|
|
$("v-name").innerHTML=`<h2>${sicher(v.name)}</h2><div class="cls">${sicher(v.klasse)} · ${v.km.toLocaleString('de-DE')} km</div>`;
|
|
$("v-status").innerHTML=`<span class="pill ${st[1]}">${st[0]}</span>`;
|
|
$("v-car").innerHTML=carSvg(v.typ,v.farbe);
|
|
const cbar=(l,val)=>`<div class="cbar"><span class="cl">${l}</span><div class="ct"><div class="cf" style="width:${val}%;background:${condColor(val)}"></div></div><span class="cv">${val}%</span></div>`;
|
|
let actions="", hint="";
|
|
if(v.status==="eingelagert"){
|
|
actions=`<button class="btn primary" data-act="aus" ${amStandort?'':'disabled'}>Ausparken</button>`;
|
|
if(!amStandort)hint=`🚫 Steht in „${garageName(v.garage).replace(/\s*\(.*\)/,'')}“ — nur dort ausparkbar.`;
|
|
}
|
|
else if(v.status==="draußen")actions=`<button class="btn ghost" data-act="orten">Orten</button><button class="btn primary" data-act="ein">Einparken (hier)</button>`;
|
|
else if(v.status==="beschlagnahmt")actions=`<button class="btn amber" data-act="ausloesen">Auslösen · 500 €</button>`;
|
|
$("v-foot").innerHTML=`
|
|
<div class="cond">${cbar("Tank",v.tank)}${cbar("Motor",v.motor)}${cbar("Karosserie",v.karosserie)}</div>
|
|
<div class="plate-wrap">${plateHtml(v.kennzeichen,v.plateStyle)}<div class="plate-note">Kennzeichenfarbe wird beim Kauf festgelegt</div></div>
|
|
<div class="actions">${hint?`<span class="ausp-hint">${hint}</span>`:''}<div style="flex:1"></div>${actions}</div>`;
|
|
$("v-foot").querySelectorAll("[data-act]").forEach(b=>b.addEventListener("click",()=>fahrzeugAktion(v,b.dataset.act)));
|
|
}
|
|
function fahrzeugAktion(v,act){
|
|
/* In FiveM entscheidet der Server: Besitz, Standort-Regel, Zugang und
|
|
Gebühren werden dort geprüft. Die Oberfläche schickt nur den Wunsch. */
|
|
if(IST_FIVEM){
|
|
if(act==="aus") nui("ausparken",{kennzeichen:v.kennzeichen,standort:standort});
|
|
else if(act==="ein") nui("einparken",{kennzeichen:v.kennzeichen,standort:standort});
|
|
else if(act==="ausloesen") nui("ausloesen",{kennzeichen:v.kennzeichen,standort:standort});
|
|
else if(act==="orten") nui("orten",{kennzeichen:v.kennzeichen});
|
|
return;
|
|
}
|
|
|
|
// Browser-Ansicht: lokal simulieren
|
|
if(act==="aus"){ if(v.garage!==standort){toast("Nur an der Garage ausparkbar, wo das Fahrzeug steht","warn");return;} v.status="draußen";toast(v.name+" ausgeparkt · "+v.kennzeichen,"good");}
|
|
else if(act==="ein"){v.status="eingelagert";v.garage=standort;toast(v.name+" eingelagert · "+garageName(standort).replace(/\s*\(.*\)/,''),"good");}
|
|
else if(act==="orten"){const orte=["Vinewood Blvd","Vespucci Beach","Route 68","Legion Square","Sandy Shores"];toast("📍 "+v.name+" zuletzt: "+orte[Math.floor(Math.random()*orte.length)]);}
|
|
else if(act==="ausloesen"){v.status="eingelagert";v.garage=standort;toast(v.name+" für 500 € ausgelöst · "+garageName(standort).replace(/\s*\(.*\)/,''),"good");}
|
|
render();
|
|
}
|
|
/* =====================================================================
|
|
NUI-BRÜCKE — Verbindung zum FiveM-Client
|
|
===================================================================== */
|
|
const IST_FIVEM = (typeof GetParentResourceName === "function");
|
|
const RESSOURCE = IST_FIVEM ? GetParentResourceName() : "aether-garage";
|
|
|
|
function nui(aktion, daten){
|
|
if(!IST_FIVEM) return Promise.resolve();
|
|
return fetch(`https://${RESSOURCE}/${aktion}`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json; charset=UTF-8" },
|
|
body: JSON.stringify(daten || {}),
|
|
}).catch(() => {});
|
|
}
|
|
|
|
/* Übernimmt Garagen und Fahrzeuge vom Server.
|
|
Der Server schickt Status als in/out/impound — hier auf die
|
|
Beschriftungen der Oberfläche abbilden. */
|
|
const STATUS_TEXT = { in: "eingelagert", out: "draußen", impound: "beschlagnahmt" };
|
|
|
|
function setzeZustand(z){
|
|
garagen = (z.garagen || []).map(g => ({
|
|
id: g.id,
|
|
name: g.name + (g.typ === "oeffentlich" ? " (öffentlich)" : " (Zugang)"),
|
|
typ: g.typ === "oeffentlich" ? "öffentlich" : "haus",
|
|
zugang: true, // der Server schickt nur zugängliche Garagen
|
|
}));
|
|
|
|
fahrzeuge = (z.fahrzeuge || []).map(v => ({
|
|
...v,
|
|
status: STATUS_TEXT[v.status] || v.status,
|
|
}));
|
|
|
|
standort = z.standort || standort;
|
|
if(!garagen.some(g => g.id === aktiveGarage)) aktiveGarage = standort;
|
|
|
|
render();
|
|
if(z.hinweis) toast(z.hinweis, "good");
|
|
}
|
|
|
|
window.addEventListener("message", (ereignis) => {
|
|
const n = ereignis.data || {};
|
|
|
|
if(n.aktion === "oeffnen"){
|
|
document.body.style.display = "";
|
|
setzeZustand(n);
|
|
}
|
|
else if(n.aktion === "zustand"){
|
|
setzeZustand(n);
|
|
}
|
|
else if(n.aktion === "hinweis"){
|
|
toast(n.text || "", "good");
|
|
}
|
|
else if(n.aktion === "fehler"){
|
|
toast(n.text || "Nicht möglich", "warn");
|
|
}
|
|
else if(n.aktion === "schliessen"){
|
|
document.body.style.display = "none";
|
|
}
|
|
});
|
|
|
|
/* ESC schließt die Garage */
|
|
document.addEventListener("keydown", (e) => {
|
|
if(e.key === "Escape" && IST_FIVEM) nui("schliessen", {});
|
|
});
|
|
|
|
/* =====================================================================
|
|
START
|
|
===================================================================== */
|
|
if(IST_FIVEM){
|
|
document.body.style.display = "none"; // wartet auf "oeffnen"
|
|
} else {
|
|
render(); // Browser-Ansicht mit den Beispieldaten oben
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|