aether-framework/immobilien/index.html
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark,
  Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI)
- aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource,
  an aether-core gekoppelt (Rechte über aether_users.admin_group), mit
  WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU)
- Prototypen admin/ vervollständigt
- SQL-Schema aether_admin.sql
- Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:55:32 +02:00

297 lines
29 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>AetherImmos — Immobilien</title>
<style>
:root{
--bg:#080b0d;--fg:#e9f0ee;--muted:#7f8c8a;--surface:rgba(22,28,29,0.72);--surface2:rgba(34,42,43,0.85);
--slot:#101615;--border:rgba(255,255,255,0.09);--accent:#2fbf7a;--ink:#042113;--accent-soft:rgba(47,191,122,0.15);
--red:#f0506e;--amber:#f5b23d;--blue:#4aa8ff;--money:#34d39a;
}
*{box-sizing:border-box;} html,body{margin:0;height:100%;}
body{min-height:100%;background:radial-gradient(1100px 700px at 18% 0%,rgba(47,191,122,0.09),transparent 55%),radial-gradient(900px 700px at 90% 100%,rgba(74,168,255,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:24px 16px;overflow-x:hidden;}
.app{position:relative;width:min(1220px,97vw);aspect-ratio:16/10;border-radius:22px;overflow:hidden;
background:linear-gradient(160deg,rgba(20,25,26,0.92),rgba(10,12,13,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;}
.head{display:flex;align-items:center;gap:14px;padding:15px 24px;border-bottom:1px solid var(--border);}
.head .logo{width:42px;height:42px;border-radius:12px;display:grid;place-items:center;font-size:20px;background:linear-gradient(150deg,var(--accent),#177a4a);box-shadow:0 8px 20px var(--accent-soft);}
.head h1{font-size:19px;margin:0;font-weight:800;letter-spacing:-0.02em;} .head h1 span{color:var(--accent);} .head h1 small{display:block;font-size:11px;color:var(--muted);font-weight:500;}
.switch{display:flex;gap:5px;margin-left:18px;background:var(--slot);border-radius:12px;padding:4px;}
.switch button{border:none;background:none;color:var(--muted);font-family:inherit;font-weight:700;font-size:13px;padding:8px 15px;border-radius:9px;cursor:pointer;}
.switch button.active{background:var(--accent);color:var(--ink);}
.wallet{margin-left:auto;display:flex;gap:10px;} .money-chip{background:var(--surface);border:1px solid var(--border);border-radius:11px;padding:7px 13px;text-align:right;} .money-chip .l{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;font-weight:800;} .money-chip .v{font-size:14px;font-weight:800;font-variant-numeric:tabular-nums;}
.body{flex:1;overflow-y:auto;padding:18px 24px;} .body::-webkit-scrollbar{width:8px;} .body::-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:21px;margin:0;font-weight:800;letter-spacing:-0.02em;} .phead p{margin:2px 0 0;color:var(--muted);font-size:12.5px;}
.filters{display:flex;gap:10px;margin-bottom:16px;flex-wrap:wrap;align-items:center;}
.seg{display:inline-flex;background:var(--slot);border-radius:10px;padding:3px;} .seg button{border:none;background:none;color:var(--muted);font-weight:700;font-size:12.5px;padding:7px 13px;border-radius:8px;cursor:pointer;font-family:inherit;} .seg button.active{background:var(--accent);color:var(--ink);}
.searchbar{display:flex;align-items:center;gap:9px;background:var(--slot);border:1px solid var(--border);border-radius:10px;padding:9px 13px;flex:1;min-width:180px;} .searchbar input{flex:1;background:none;border:none;color:var(--fg);font-family:inherit;font-size:13.5px;outline:none;}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;}
.card{background:var(--surface);border:1px solid var(--border);border-radius:16px;overflow:hidden;cursor:pointer;transition:transform .12s,border-color .12s;}
.card:hover{transform:translateY(-3px);border-color:var(--accent);}
.card .photo{aspect-ratio:16/10;position:relative;overflow:hidden;} .card .photo svg{width:100%;height:100%;display:block;}
.card .ptag{position:absolute;top:10px;left:10px;background:rgba(0,0,0,0.55);color:#fff;font-size:11px;font-weight:800;padding:4px 9px;border-radius:999px;}
.card .price{position:absolute;bottom:10px;right:10px;background:var(--accent);color:var(--ink);font-size:15px;font-weight:800;padding:5px 11px;border-radius:9px;font-variant-numeric:tabular-nums;}
.card .cinfo{padding:13px 15px;} .card .ct{font-size:15px;font-weight:800;} .card .cl{font-size:12.5px;color:var(--muted);margin-top:1px;}
.card .cstats{display:flex;gap:14px;margin-top:9px;font-size:12px;color:var(--muted);font-weight:600;} .card .cstats b{color:var(--fg);}
/* Detail */
.detail-top{display:grid;grid-template-columns:1.4fr 1fr;gap:20px;align-items:start;}
.detail-photo{border-radius:16px;overflow:hidden;border:1px solid var(--border);} .detail-photo svg{width:100%;display:block;}
.detail-side .price-big{font-size:30px;font-weight:800;font-variant-numeric:tabular-nums;} .detail-side .price-big small{font-size:13px;color:var(--muted);font-weight:600;}
.kv{display:flex;justify-content:space-between;font-size:13px;padding:7px 0;border-bottom:1px solid var(--border);} .kv:last-child{border:none;} .kv span:first-child{color:var(--muted);}
.chips{display:flex;flex-wrap:wrap;gap:7px;margin-top:6px;} .chip{background:var(--slot);border:1px solid var(--border);border-radius:999px;padding:5px 11px;font-size:11.5px;font-weight:600;display:inline-flex;gap:6px;align-items:center;}
.chip::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--accent);}
.desc{color:#c2cdca;font-size:13.5px;line-height:1.6;margin-top:12px;}
.card2{background:var(--surface);border:1px solid var(--border);border-radius:15px;padding:16px;}
.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;}
.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 .av{width:34px;height:34px;border-radius:50%;display:grid;place-items:center;font-weight:800;color:#fff;flex-shrink:0;} .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);}
.pill{font-size:10px;font-weight:800;padding:3px 8px;border-radius:999px;} .pill.own{background:rgba(47,191,122,0.16);color:var(--accent);} .pill.key{background:rgba(245,178,61,0.16);color:var(--amber);}
.btn{border:none;border-radius:11px;padding:12px 18px;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.red{background:var(--red);color:#2a0910;} .btn.sm{padding:8px 13px;font-size:12.5px;} .btn:active{transform:scale(0.98);} .btn:disabled{opacity:.4;cursor:not-allowed;}
.back{background:var(--surface2);border:1px solid var(--border);color:var(--fg);border-radius:10px;padding:8px 13px;font-family:inherit;font-weight:700;cursor:pointer;}
.input,select.input{width:100%;background:var(--slot);border:1px solid var(--border);border-radius:10px;padding:11px 12px;color:var(--fg);font-size:14px;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:12px 2px 6px;display:block;}
.overlay{position:absolute;inset:0;z-index:40;background:rgba(4,6,8,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:96%;max-height:90%;overflow-y:auto;background:rgba(18,22,23,0.98);border:1px solid var(--border);border-radius:18px;padding:22px;box-shadow:0 30px 70px -18px rgba(0,0,0,0.8);}
.modal h3{margin:0 0 3px;font-size:19px;} .modal .msub{color:var(--muted);font-size:12.5px;margin-bottom:14px;} .modal-actions{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-top:16px;}
.pay{display:flex;background:var(--slot);border-radius:10px;padding:4px;margin-top:6px;} .pay button{flex:1;border:none;background:none;color:var(--muted);font-family:inherit;font-weight:700;font-size:13px;padding:9px;border-radius:8px;cursor:pointer;} .pay button.active{background:var(--accent);color:var(--ink);}
.chk{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:9px;background:var(--slot);border:1px solid var(--border);margin-bottom:5px;cursor:pointer;} .chk .av{width:28px;height:28px;border-radius:50%;display:grid;place-items:center;font-weight:800;color:#fff;font-size:11px;}
.toast{position:absolute;bottom:18px;left:50%;transform:translateX(-50%) translateY(16px);background:rgba(12,16,17,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:50;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(--accent);}
.empty{text-align:center;color:var(--muted);padding:44px 10px;} .empty .big{font-size:44px;margin-bottom:8px;}
</style>
<div class="app">
<div class="head"><span class="logo">🏡</span><h1>Aether<span>Immos</span><small>Immobilien in Los Santos</small></h1>
<div class="switch" id="switch"></div>
<div class="wallet"><div class="money-chip"><div class="l">Bargeld</div><div class="v" id="w-cash"></div></div><div class="money-chip"><div class="l">Konto</div><div class="v" id="w-bank"></div></div></div>
</div>
<div class="body" id="body"></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=>n.toLocaleString("de-DE")+" €";
const initialen=n=>n.replace(/\s*\(.*\)/,"").split(" ").map(w=>w[0]).slice(0,2).join("");
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){$("overlay").innerHTML=`<div class="modal">${html}</div>`;$("overlay").classList.add("show");}
function closeModal(){$("overlay").classList.remove("show");$("overlay").innerHTML="";}
/* =====================================================================
DATEN
===================================================================== */
const OWNER="Max Weber";
const RUECKKAUF=0.6; // Verkauf gibt 60% des Kaufpreises zurück
const START_SCHLUESSEL=5; // Anzahl Schlüssel beim Kauf
const NACHMACH_GEBUEHR=250; // Kosten fürs Nachmachen (Schlüsseldienst)
const spieler={bargeld:45000,konto:380000};
// Bekannte Personen (Schlüssel-Vergabe)
const kontakte=["Lena Berger","Ben Fischer","Karl Mendez","Sofia Klein"];
// Marktangebote (AetherImmos)
const marktplatz=[
{id:"m1",titel:"Apartment Vinewood",typ:"Wohnung",ort:"Vinewood Blvd",preis:85000,zimmer:2,flaeche:65,garage:1,farbe:"#2f7bbf",features:["Balkon","Möbliert","Aufzug"],beschreibung:"Helle 2-Zimmer-Wohnung mit Balkon im Herzen von Vinewood. Ideal für Einsteiger."},
{id:"m2",titel:"Stadthaus Mirror Park",typ:"Haus",ort:"Mirror Park",preis:210000,zimmer:4,flaeche:140,garage:2,farbe:"#bf6a2f",features:["Garten","Garage","Kamin"],beschreibung:"Charmantes Stadthaus mit Garten und Doppelgarage in ruhiger Lage."},
{id:"m3",titel:"Villa Vinewood Hills",typ:"Villa",ort:"Vinewood Hills",preis:1200000,zimmer:7,flaeche:420,garage:4,farbe:"#8a6a34",features:["Pool","Meerblick","Sicherheit","Garage"],beschreibung:"Exklusive Luxusvilla mit Infinity-Pool, Panoramablick und Sicherheitsanlage."},
{id:"m4",titel:"Loft Downtown",typ:"Wohnung",ort:"Downtown",preis:145000,zimmer:1,flaeche:90,garage:0,farbe:"#5a5a66",features:["Dachterrasse","Möbliert"],beschreibung:"Modernes Industrie-Loft mit offener Raumaufteilung und Dachterrasse."},
{id:"m5",titel:"Gewerbe Legion Square",typ:"Gewerbe",ort:"Legion Square",preis:320000,zimmer:0,flaeche:200,garage:1,farbe:"#2f9e73",features:["Ladenfläche","Lager","Vitrine"],beschreibung:"Attraktive Gewerbefläche in Top-Lauflage – perfekt für Einzelhandel."},
{id:"m6",titel:"Bungalow Sandy Shores",typ:"Haus",ort:"Sandy Shores",preis:68000,zimmer:3,flaeche:110,garage:1,farbe:"#c79a5a",features:["Garten","Werkstatt"],beschreibung:"Gemütlicher Bungalow mit Werkstatt – viel Platz fürs Geld."},
{id:"m7",titel:"Penthouse Del Perro",typ:"Wohnung",ort:"Del Perro",preis:890000,zimmer:3,flaeche:210,garage:2,farbe:"#7c5cf6",features:["Pool","Meerblick","Aufzug","Sicherheit"],beschreibung:"Spektakuläres Penthouse mit privatem Pool und atemberaubendem Meerblick."}
];
// Eigene Immobilien (mit Schlüssel-Zugang; Schlüssel = Item im Inventar)
const meine=[
{id:"o1",titel:"Apartment 4B",typ:"Wohnung",ort:"Vespucci Blvd",kaufpreis:95000,zimmer:2,flaeche:70,garage:1,farbe:"#2f7bbf",features:["Balkon","Garage","Möbliert"],beschreibung:"Deine erste eigene Wohnung in Vespucci.",schluessel:5,gekauft:"20.05.2026"}
];
let ansicht="markt"; // "markt" | "meine"
let filterTyp="Alle";
let suche="";
let marktDetail=null; // geöffnetes Angebot
let meineDetail=null; // geöffnete eigene Immobilie
/* =====================================================================
IMMOBILIEN-ILLUSTRATION (stilisiertes "Foto" je Typ)
===================================================================== */
function immoSvg(typ,farbe){
const dach=schattiere(farbe,0.62), wand=farbe, wandD=schattiere(farbe,0.82), glas="#bfe0f0", glasD="#8fbcd6";
const himmel=`<rect width="300" height="188" fill="url(#sky)"/><circle cx="252" cy="40" r="20" fill="#ffe3a0" opacity="0.8"/><rect y="150" width="300" height="38" fill="#20302a"/>`;
const fenster=(x,y,w,h)=>`<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="2" fill="${glas}"/><rect x="${x}" y="${y}" width="${w}" height="${h}" rx="2" fill="none" stroke="${glasD}" stroke-width="1"/><line x1="${x+w/2}" y1="${y}" x2="${x+w/2}" y2="${y+h}" stroke="${glasD}" stroke-width="1"/>`;
let g="";
if(typ==="Villa"){
g=`<rect x="34" y="88" width="232" height="64" fill="${wand}"/><rect x="34" y="82" width="232" height="8" fill="${dach}"/>
<rect x="44" y="100" width="46" height="44" fill="${wandD}"/>${fenster(100,100,40,30)}${fenster(150,100,40,30)}${fenster(200,100,40,30)}
<rect x="120" y="120" width="24" height="32" fill="${dach}"/><ellipse cx="70" cy="150" rx="34" ry="7" fill="#2a7bbf" opacity="0.55"/>`;
} else if(typ==="Haus"){
g=`<path d="M60 98 L150 56 L240 98 Z" fill="${dach}"/><rect x="78" y="98" width="144" height="54" fill="${wand}"/>
<rect x="132" y="118" width="26" height="34" fill="${dach}"/>${fenster(92,110,30,24)}${fenster(178,110,30,24)}<rect x="196" y="60" width="12" height="24" fill="${dach}"/>`;
} else if(typ==="Gewerbe"){
g=`<rect x="40" y="86" width="220" height="66" fill="${wand}"/><rect x="40" y="80" width="220" height="10" fill="${dach}"/>
<rect x="50" y="120" width="200" height="32" fill="${glasD}"/>${fenster(56,124,40,24)}${fenster(102,124,40,24)}${fenster(148,124,40,24)}${fenster(194,124,40,24)}
<path d="M46 108 h208 l-10 12 h-188 Z" fill="${schattiere(farbe,1.1)}"/><rect x="120" y="90" width="60" height="14" rx="3" fill="${dach}"/>`;
} else { // Wohnung / Penthouse
g=`<rect x="70" y="46" width="160" height="106" fill="${wand}"/><rect x="70" y="42" width="160" height="8" fill="${dach}"/>`;
for(let r=0;r<4;r++)for(let c=0;c<4;c++)g+=fenster(84+c*38,56+r*24,26,16);
g+=`<rect x="132" y="130" width="30" height="22" fill="${dach}"/>`;
}
return `<svg viewBox="0 0 300 188"><defs><linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#25506b"/><stop offset="100%" stop-color="#3a7a8a"/></linearGradient></defs>${himmel}${g}</svg>`;
}
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);}
/* =====================================================================
SHELL
===================================================================== */
function renderSwitch(){
$("switch").innerHTML=`<button data-a="markt" class="${ansicht==='markt'?'active':''}">Markt</button><button data-a="meine" class="${ansicht==='meine'?'active':''}">Meine Immobilien${meine.length?' ('+meine.length+')':''}</button>`;
$("switch").querySelectorAll("[data-a]").forEach(b=>b.addEventListener("click",()=>{ansicht=b.dataset.a;marktDetail=null;meineDetail=null;render();}));
}
function aktualisiereGeld(){$("w-cash").textContent=euro(spieler.bargeld);$("w-bank").textContent=euro(spieler.konto);}
function render(){renderSwitch();aktualisiereGeld();(ansicht==="markt"?renderMarkt:renderMeine)();}
/* =====================================================================
MARKT (AetherImmos)
===================================================================== */
function renderMarkt(){
if(marktDetail){renderMarktDetail();return;}
const typen=["Alle","Wohnung","Haus","Villa","Gewerbe"];
const liste=marktplatz.filter(m=>(filterTyp==="Alle"||m.typ===filterTyp)&&(!suche||(m.titel+" "+m.ort).toLowerCase().includes(suche)));
$("body").innerHTML=`<div class="phead"><div><h2>Immobilienmarkt</h2><p>${marktplatz.length} Angebote in Los Santos</p></div></div>
<div class="filters"><div class="seg" id="f-typ">${typen.map(t=>`<button data-t="${t}" class="${t===filterTyp?'active':''}">${t}</button>`).join('')}</div>
<div class="searchbar"><span>🔎</span><input id="f-such" placeholder="Ort oder Titel suchen…" value="${sicher(suche)}"></div></div>
<div class="grid" id="markt-grid">${liste.map(marktCard).join('')||'<div class="empty" style="grid-column:1/-1"><div class="big">🏙️</div>Keine passenden Angebote.</div>'}</div>`;
$("f-typ").querySelectorAll("[data-t]").forEach(b=>b.addEventListener("click",()=>{filterTyp=b.dataset.t;renderMarkt();}));
const s=$("f-such");s.addEventListener("input",()=>{suche=s.value.trim().toLowerCase();const g=$("markt-grid");const l=marktplatz.filter(m=>(filterTyp==="Alle"||m.typ===filterTyp)&&(!suche||(m.titel+" "+m.ort).toLowerCase().includes(suche)));g.innerHTML=l.map(marktCard).join('')||'<div class="empty" style="grid-column:1/-1">Keine Treffer.</div>';bindMarktCards();});
bindMarktCards();
}
function marktCard(m){return `<div class="card" data-m="${m.id}"><div class="photo">${immoSvg(m.typ,m.farbe)}<span class="ptag">${m.typ}</span><span class="price">${euro(m.preis)}</span></div>
<div class="cinfo"><div class="ct">${sicher(m.titel)}</div><div class="cl">📍 ${sicher(m.ort)}</div>
<div class="cstats">${m.zimmer?`<span><b>${m.zimmer}</b> Zi.</span>`:''}<span><b>${m.flaeche}</b> m²</span><span><b>${m.garage}</b> 🚗</span></div></div></div>`;}
function bindMarktCards(){$("body").querySelectorAll("[data-m]").forEach(c=>c.addEventListener("click",()=>{marktDetail=c.dataset.m;renderMarktDetail();}));}
function renderMarktDetail(){
const m=marktplatz.find(x=>x.id===marktDetail);
$("body").innerHTML=`<div class="phead"><div style="display:flex;align-items:center;gap:12px"><button class="back" id="md-back">‹ Markt</button><h2 style="margin:0">${sicher(m.titel)}</h2></div></div>
<div class="detail-top"><div class="detail-photo">${immoSvg(m.typ,m.farbe)}</div>
<div class="detail-side"><div class="price-big">${euro(m.preis)} <small>Kaufpreis</small></div>
<div style="margin:14px 0">
<div class="kv"><span>Typ</span><span>${m.typ}</span></div><div class="kv"><span>Lage</span><span>${sicher(m.ort)}</span></div>
<div class="kv"><span>Zimmer</span><span>${m.zimmer||'—'}</span></div><div class="kv"><span>Wohnfläche</span><span>${m.flaeche} m²</span></div>
<div class="kv"><span>Garagenplätze</span><span>${m.garage}</span></div></div>
<button class="btn primary" id="md-buy" style="width:100%">Kaufen · ${euro(m.preis)}</button></div></div>
<div class="card2" style="margin-top:16px"><div class="list-h">Ausstattung</div><div class="chips">${m.features.map(f=>`<span class="chip">${sicher(f)}</span>`).join('')}</div>
<div class="desc">${sicher(m.beschreibung)}</div></div>`;
$("md-back").addEventListener("click",()=>{marktDetail=null;renderMarkt();});
$("md-buy").addEventListener("click",()=>kaufMenu(m));
}
/* =====================================================================
KAUFMENÜ (separat)
===================================================================== */
let kaufZahlung="konto";
function kaufMenu(m){
kaufZahlung="konto";
const notar=Math.round(m.preis*0.02);
modal(`<h3>Kaufmenü</h3><div class="msub">${sicher(m.titel)} · ${sicher(m.ort)}</div>
<div style="border-radius:12px;overflow:hidden;border:1px solid var(--border);margin-bottom:12px">${immoSvg(m.typ,m.farbe)}</div>
<div class="kv"><span>Kaufpreis</span><span>${euro(m.preis)}</span></div>
<div class="kv"><span>Notar- &amp; Bearbeitungsgebühr (2%)</span><span>${euro(notar)}</span></div>
<div class="kv" style="font-weight:800"><span>Gesamt</span><span>${euro(m.preis+notar)}</span></div>
<div style="font-size:11.5px;color:var(--muted);margin-top:8px">Beim Kauf erhältst du <b>${START_SCHLUESSEL} Hausschlüssel (Items)</b>, die du nachmachen und weitergeben kannst.</div>
<label class="flabel">Zahlung</label><div class="pay" id="k-pay"><button data-p="konto" class="active">💳 Konto (${euro(spieler.konto)})</button><button data-p="bargeld">💵 Bargeld (${euro(spieler.bargeld)})</button></div>
<div class="modal-actions"><button class="btn ghost" id="k-cancel">Abbrechen</button><button class="btn primary" id="k-ok">Kauf bestätigen</button></div>`);
const gesamt=m.preis+notar;
$("k-pay").querySelectorAll("[data-p]").forEach(b=>b.addEventListener("click",()=>{kaufZahlung=b.dataset.p;$("k-pay").querySelectorAll("[data-p]").forEach(x=>x.classList.remove("active"));b.classList.add("active");}));
$("k-cancel").addEventListener("click",closeModal);
$("k-ok").addEventListener("click",()=>{
const guthaben=kaufZahlung==="konto"?spieler.konto:spieler.bargeld;
if(gesamt>guthaben){toast(kaufZahlung==="konto"?"Konto nicht gedeckt":"Nicht genug Bargeld","warn");return;}
if(kaufZahlung==="konto")spieler.konto-=gesamt;else spieler.bargeld-=gesamt;
// Angebot vom Markt entfernen, in Besitz überführen, Schlüssel-Item vergeben
marktplatz.splice(marktplatz.findIndex(x=>x.id===m.id),1);
meine.push({id:"o"+Date.now(),titel:m.titel,typ:m.typ,ort:m.ort,kaufpreis:m.preis,zimmer:m.zimmer,flaeche:m.flaeche,garage:m.garage,farbe:m.farbe,features:m.features,beschreibung:m.beschreibung,schluessel:START_SCHLUESSEL,gekauft:new Date().toLocaleDateString('de-DE')});
closeModal();toast("🏡 "+m.titel+" gekauft! "+START_SCHLUESSEL+" Schlüssel erhalten.","good");
ansicht="meine";marktDetail=null;render();
});
}
/* =====================================================================
MEINE IMMOBILIEN (Verwaltung, Zugang/Schlüssel, Verkauf)
===================================================================== */
function renderMeine(){
if(meineDetail){renderMeineDetail();return;}
$("body").innerHTML=`<div class="phead"><div><h2>Meine Immobilien</h2><p>${meine.length} in deinem Besitz</p></div></div>
${meine.length?`<div class="grid">${meine.map(o=>`<div class="card" data-o="${o.id}"><div class="photo">${immoSvg(o.typ,o.farbe)}<span class="ptag">${o.typ}</span></div>
<div class="cinfo"><div class="ct">${sicher(o.titel)}</div><div class="cl">📍 ${sicher(o.ort)} · ${o.schluessel} Schlüssel</div>
<div class="cstats">${o.zimmer?`<span><b>${o.zimmer}</b> Zi.</span>`:''}<span><b>${o.flaeche}</b> m²</span><span><b>${o.garage}</b> 🚗</span></div></div></div>`).join('')}</div>`
:'<div class="empty"><div class="big">🏠</div>Du besitzt noch keine Immobilie.<div style="margin-top:12px"><button class="btn primary" id="zum-markt">Zum Markt</button></div></div>'}`;
$("body").querySelectorAll("[data-o]").forEach(c=>c.addEventListener("click",()=>{meineDetail=c.dataset.o;renderMeineDetail();}));
const zm=$("zum-markt");if(zm)zm.addEventListener("click",()=>{ansicht="markt";render();});
}
function renderMeineDetail(){
const o=meine.find(x=>x.id===meineDetail);
const verkaufswert=Math.round(o.kaufpreis*RUECKKAUF);
$("body").innerHTML=`<div class="phead"><div style="display:flex;align-items:center;gap:12px"><button class="back" id="o-back">‹ Meine Immobilien</button><h2 style="margin:0">${sicher(o.titel)}</h2><span class="pill own">Eigentum</span></div>
<button class="btn primary" id="o-enter">Betreten</button></div>
<div class="detail-top"><div class="detail-photo">${immoSvg(o.typ,o.farbe)}</div>
<div class="detail-side">
<div class="kv"><span>Typ</span><span>${o.typ} · ${sicher(o.ort)}</span></div>
<div class="kv"><span>Wohnfläche</span><span>${o.flaeche} m² · ${o.zimmer||'—'} Zi.</span></div>
<div class="kv"><span>Gekauft am</span><span>${o.gekauft} für ${euro(o.kaufpreis)}</span></div>
<div class="kv"><span>Verkaufswert (${Math.round(RUECKKAUF*100)}%)</span><span style="color:var(--money);font-weight:800">${euro(verkaufswert)}</span></div>
<div class="chips" style="margin-top:12px">${o.features.map(f=>`<span class="chip">${sicher(f)}</span>`).join('')}</div>
<button class="btn red" id="o-sell" style="width:100%;margin-top:16px">Verkaufen · ${euro(verkaufswert)} zurück</button></div></div>
<div class="card2" style="margin-top:16px"><div class="list-h">Schlüssel (Items) <span class="r">Du hältst: <b style="color:var(--amber)">${o.schluessel}</b></span></div>
<div class="row" style="margin:0"><span class="av" style="background:#f5b23d;color:#3a2600">🔑</span>
<div class="grow"><div class="t">Hausschlüssel „${sicher(o.titel)}“</div><div class="m">Kann nachgemacht und als Item weitergegeben werden</div></div></div>
<div style="display:flex;gap:9px;margin-top:10px"><button class="btn primary sm" id="o-copy">+ Nachmachen · ${euro(NACHMACH_GEBUEHR)}</button></div>
<div style="font-size:11.5px;color:var(--muted);margin-top:8px">Beim Kauf erhältst du ${START_SCHLUESSEL} Schlüssel. „Nachmachen" erstellt beim Schlüsseldienst eine Kopie. Weitergeben an andere geschieht über das Inventar (Schlüssel = Item).</div></div>`;
$("o-back").addEventListener("click",()=>{meineDetail=null;renderMeine();});
$("o-enter").addEventListener("click",()=>toast("🚪 "+o.titel+" betreten"));
$("o-sell").addEventListener("click",()=>verkaufFrage(o));
$("o-copy").addEventListener("click",()=>{
if(spieler.konto>=NACHMACH_GEBUEHR)spieler.konto-=NACHMACH_GEBUEHR;
else if(spieler.bargeld>=NACHMACH_GEBUEHR)spieler.bargeld-=NACHMACH_GEBUEHR;
else {toast("Nicht genug Geld für den Schlüsseldienst","warn");return;}
o.schluessel++;toast("🔑 Schlüssel nachgemacht","good");aktualisiereGeld();renderMeineDetail();});
}
function verkaufFrage(o){
const wert=Math.round(o.kaufpreis*RUECKKAUF);
modal(`<h3>Immobilie verkaufen</h3><div class="msub">${sicher(o.titel)}</div>
<div class="kv"><span>Ursprünglicher Kaufpreis</span><span>${euro(o.kaufpreis)}</span></div>
<div class="kv"><span>Rückkaufquote</span><span>${Math.round(RUECKKAUF*100)} %</span></div>
<div class="kv" style="font-weight:800"><span>Du erhältst</span><span style="color:var(--money)">${euro(wert)}</span></div>
<div style="font-size:11.5px;color:var(--muted);margin-top:8px">Der Verkauf erstattet nur einen Teil des Kaufpreises. Alle Schlüssel werden ungültig.</div>
<label class="flabel">Auszahlung auf</label><div class="pay" id="v-pay"><button data-p="konto" class="active">💳 Konto</button><button data-p="bargeld">💵 Bargeld</button></div>
<div class="modal-actions"><button class="btn ghost" id="v-cancel">Abbrechen</button><button class="btn red" id="v-ok">Verkaufen</button></div>`);
let ziel="konto";
$("v-pay").querySelectorAll("[data-p]").forEach(b=>b.addEventListener("click",()=>{ziel=b.dataset.p;$("v-pay").querySelectorAll("[data-p]").forEach(x=>x.classList.remove("active"));b.classList.add("active");}));
$("v-cancel").addEventListener("click",closeModal);
$("v-ok").addEventListener("click",()=>{
if(ziel==="konto")spieler.konto+=wert;else spieler.bargeld+=wert;
meine.splice(meine.findIndex(x=>x.id===o.id),1);
closeModal();toast("Verkauft für "+euro(wert)+" · Schlüssel ungültig","good");
meineDetail=null;render();
});
}
$("overlay").addEventListener("click",e=>{if(e.target===$("overlay"))closeModal();});
render();
</script>
</body>
</html>