aether-framework/resources/[aether]/aether-shop/html/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

541 lines
33 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>Aether — Shop</title>
<style>
/* ===================== Tokens ===================== */
:root {
--page-bg: #0a0b10;
--fg: #e9ecf4;
--muted: #858ca1;
--surface: rgba(24,26,36,0.72);
--surface2: rgba(38,41,55,0.82);
--slot: #14161f;
--border: rgba(255,255,255,0.09);
--accent: #4ade80; /* wird je Shop gesetzt */
--accent-ink: #06210f;
--accent-soft: rgba(74,222,128,0.16);
--red: #f0506e;
--amber: #f5b23d;
--money: #4ade80;
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body {
min-height: 100%;
background:
radial-gradient(1100px 700px at 12% 8%, var(--accent-soft), transparent 55%),
radial-gradient(1000px 800px at 88% 92%, rgba(74,168,255,0.06), transparent 55%),
var(--page-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: 30px 18px; overflow-x: hidden;
transition: background .4s ease;
}
/* ===================== Shop-Fenster ===================== */
.shop {
position: relative; width: min(1180px, 96vw); aspect-ratio: 16 / 10;
background: linear-gradient(160deg, rgba(18,20,28,0.92), rgba(10,11,16,0.96));
backdrop-filter: blur(20px); border: 1px solid var(--border); border-radius: 22px;
box-shadow: 0 50px 100px -30px rgba(0,0,0,0.82); overflow: hidden; display: flex; flex-direction: column;
}
.shop-head { display: flex; align-items: center; gap: 16px; padding: 15px 24px; border-bottom: 1px solid var(--border); }
.shop-logo { width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center; font-size: 22px;
background: linear-gradient(150deg, var(--accent), color-mix(in srgb, var(--accent) 65%, #000)); box-shadow: 0 8px 22px var(--accent-soft); transition: background .3s; }
.shop-title h1 { font-size: 19px; margin: 0; font-weight: 800; letter-spacing: -0.02em; }
.shop-title .tl { font-size: 12px; color: var(--muted); }
.shop-title .open { color: var(--accent); font-weight: 700; }
/* Shop-Wechsel */
.shop-switch { display: flex; gap: 6px; margin-left: 10px; background: var(--slot); padding: 4px; border-radius: 12px; }
.shop-switch button { border: none; background: none; color: var(--muted); font-family: inherit; font-size: 13px; font-weight: 700;
padding: 7px 13px; border-radius: 8px; cursor: pointer; transition: .12s; }
.shop-switch button.active { background: var(--accent); color: var(--accent-ink); }
.wallet-info { margin-left: auto; display: flex; gap: 10px; }
.money-chip { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 8px 14px; text-align: right; }
.money-chip .l { font-size: 10.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; font-weight: 700; }
.money-chip .v { font-size: 15px; font-weight: 800; font-variant-numeric: tabular-nums; }
.shop-body { flex: 1; display: grid; grid-template-columns: 190px 1fr 330px; min-height: 0; }
/* Kategorien */
.cats { border-right: 1px solid var(--border); padding: 14px 12px; display: flex; flex-direction: column; gap: 4px; overflow-y: auto; }
.cats button { display: flex; align-items: center; gap: 11px; padding: 10px 12px; border-radius: 11px; border: none;
background: none; color: var(--muted); font-family: inherit; font-size: 13.5px; font-weight: 700; cursor: pointer; text-align: left; transition: .12s; }
.cats button .ci { font-size: 16px; width: 20px; text-align: center; }
.cats button .cc { margin-left: auto; font-size: 11px; opacity: 0.7; font-weight: 600; }
.cats button:hover { background: var(--surface); color: var(--fg); }
.cats button.active { background: var(--accent-soft); color: var(--accent); }
/* Produkt-Bereich */
.prods { padding: 16px 18px; overflow-y: auto; display: flex; flex-direction: column; }
.prods::-webkit-scrollbar, .cart-list::-webkit-scrollbar { width: 7px; }
.prods::-webkit-scrollbar-thumb, .cart-list::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 4px; }
.searchbar { display: flex; align-items: center; gap: 10px; background: var(--slot); border: 1px solid var(--border);
border-radius: 12px; padding: 10px 14px; margin-bottom: 14px; }
.searchbar input { flex: 1; background: none; border: none; color: var(--fg); font-family: inherit; font-size: 14px; outline: none; }
.prod-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); gap: 12px; }
.prod { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; padding: 14px; position: relative;
display: flex; flex-direction: column; gap: 8px; transition: transform .12s, border-color .12s; }
.prod:hover { transform: translateY(-3px); border-color: var(--accent); }
.prod .badge-sale { position: absolute; top: 10px; right: 10px; background: var(--red); color: #fff; font-size: 10px;
font-weight: 800; padding: 3px 8px; border-radius: 999px; }
.prod .badge-lic { position: absolute; top: 10px; left: 10px; background: rgba(245,178,61,0.16); color: var(--amber);
font-size: 9.5px; font-weight: 800; padding: 3px 7px; border-radius: 999px; }
.prod .picon { width: 100%; aspect-ratio: 1.6; border-radius: 11px; display: grid; place-items: center; font-size: 42px;
background: linear-gradient(160deg, rgba(255,255,255,0.06), rgba(255,255,255,0.01)); }
.prod .pname { font-size: 14px; font-weight: 700; line-height: 1.2; }
.prod .pdesc { font-size: 11.5px; color: var(--muted); line-height: 1.35; flex: 1; }
.prod .pmeta { display: flex; align-items: center; justify-content: space-between; }
.prod .price { font-size: 17px; font-weight: 800; font-variant-numeric: tabular-nums; }
.prod .price s { font-size: 11px; color: var(--muted); font-weight: 500; margin-right: 5px; }
.prod .stock { font-size: 10.5px; color: var(--muted); }
.prod .stock.out { color: var(--red); font-weight: 700; }
.prod .add { border: none; background: var(--accent); color: var(--accent-ink); border-radius: 10px; padding: 9px;
font-weight: 800; font-family: inherit; font-size: 13px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; transition: .12s; }
.prod .add:hover { filter: brightness(1.08); }
.prod .add:disabled { background: var(--surface2); color: var(--muted); cursor: not-allowed; }
.prod .incart { position: absolute; top: 10px; right: 10px; background: var(--accent); color: var(--accent-ink);
min-width: 22px; height: 22px; border-radius: 999px; display: grid; place-items: center; font-size: 12px; font-weight: 800; padding: 0 6px; }
/* Warenkorb */
.cart { border-left: 1px solid var(--border); display: flex; flex-direction: column; min-height: 0; }
.cart-head { padding: 16px 18px 12px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--border); }
.cart-head .h { font-size: 16px; font-weight: 800; }
.cart-head .clr { background: none; border: none; color: var(--muted); font-family: inherit; font-size: 12px; cursor: pointer; font-weight: 600; }
.cart-head .clr:hover { color: var(--red); }
.cart-list { flex: 1; overflow-y: auto; padding: 12px 16px; display: flex; flex-direction: column; gap: 8px; }
.cart-empty { margin: auto; text-align: center; color: var(--muted); }
.cart-empty .big { font-size: 44px; margin-bottom: 8px; }
.citem { display: flex; align-items: center; gap: 11px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 9px 11px; }
.citem .ci { font-size: 22px; }
.citem .cg { flex: 1; min-width: 0; }
.citem .cn { font-size: 13px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.citem .cp { font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.stepper { display: flex; align-items: center; gap: 0; background: var(--slot); border-radius: 9px; overflow: hidden; }
.stepper button { width: 26px; height: 26px; border: none; background: none; color: var(--fg); font-size: 16px; cursor: pointer; font-family: inherit; }
.stepper button:hover { background: var(--surface2); }
.stepper .q { min-width: 22px; text-align: center; font-size: 13px; font-weight: 800; font-variant-numeric: tabular-nums; }
.cart-foot { border-top: 1px solid var(--border); padding: 14px 18px 16px; display: flex; flex-direction: column; gap: 11px; }
.sumrow { display: flex; justify-content: space-between; font-size: 13px; color: var(--muted); }
.sumrow.total { font-size: 20px; color: var(--fg); font-weight: 800; }
.sumrow .v { font-variant-numeric: tabular-nums; }
.pay { display: flex; background: var(--slot); border-radius: 11px; padding: 4px; }
.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; display: flex; align-items: center; justify-content: center; gap: 7px; }
.pay button.active { background: var(--accent); color: var(--accent-ink); }
.checkout { border: none; background: var(--accent); color: var(--accent-ink); border-radius: 13px; padding: 15px;
font-size: 15.5px; font-weight: 800; cursor: pointer; font-family: inherit; display: flex; justify-content: space-between; padding-inline: 20px; }
.checkout:hover { filter: brightness(1.08); }
.checkout:disabled { background: var(--surface2); color: var(--muted); cursor: not-allowed; }
/* Beleg / Toast */
.overlay { position: absolute; inset: 0; z-index: 30; background: rgba(6,7,11,0.7); backdrop-filter: blur(6px);
display: none; align-items: center; justify-content: center; }
.overlay.show { display: flex; }
.receipt { width: 340px; max-width: 92%; background: #f6f3ec; color: #1c1a15; border-radius: 12px; padding: 22px 24px 20px;
box-shadow: 0 30px 70px -18px rgba(0,0,0,0.8); font-family: ui-monospace, "SF Mono", monospace; position: relative;
animation: pop .35s ease; }
@keyframes pop { from { transform: scale(.9); opacity: 0; } to { transform: none; opacity: 1; } }
.receipt::after { content: ""; position: absolute; left: 0; right: 0; bottom: -8px; height: 8px;
background: repeating-linear-gradient(135deg, #f6f3ec 0 8px, transparent 8px 16px); filter: drop-shadow(0 2px 1px rgba(0,0,0,0.1)); }
.receipt h3 { text-align: center; margin: 0 0 2px; font-size: 16px; letter-spacing: 0.1em; }
.receipt .rsub { text-align: center; font-size: 11px; color: #6b6455; margin-bottom: 14px; }
.receipt .rline { display: flex; justify-content: space-between; font-size: 12px; padding: 3px 0; }
.receipt .rdiv { border-top: 1px dashed #b4ac97; margin: 10px 0; }
.receipt .rtotal { display: flex; justify-content: space-between; font-size: 15px; font-weight: 700; }
.receipt .rthanks { text-align: center; margin-top: 14px; font-size: 11px; color: #6b6455; }
.receipt .rclose { margin-top: 16px; width: 100%; border: none; background: #1c1a15; color: #f6f3ec; border-radius: 9px;
padding: 11px; font-family: inherit; font-weight: 700; cursor: pointer; }
.toast { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%) translateY(18px);
background: rgba(12,14,20,0.97); border: 1px solid var(--border); color: var(--fg); padding: 11px 20px;
border-radius: 13px; 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); }
</style>
<div class="shop" id="shop">
<div class="shop-head">
<span class="shop-logo" id="shop-logo">🛒</span>
<div class="shop-title">
<h1 id="shop-name">LS 24/7</h1>
<div class="tl"><span class="open">● Geöffnet</span> · <span id="shop-tl">Alles für den Alltag</span></div>
</div>
<div class="shop-switch" id="shop-switch"></div>
<div class="wallet-info">
<div class="money-chip"><div class="l">Bargeld</div><div class="v" id="bal-cash"></div></div>
<div class="money-chip"><div class="l">Konto</div><div class="v" id="bal-bank"></div></div>
</div>
</div>
<div class="shop-body">
<div class="cats" id="cats"></div>
<div class="prods">
<div class="searchbar"><span>🔎</span><input id="search" placeholder="Produkt suchen…" autocomplete="off"></div>
<div class="prod-grid" id="prod-grid"></div>
</div>
<div class="cart">
<div class="cart-head"><span class="h">Warenkorb <span id="cart-count"></span></span><button class="clr" id="cart-clear">Leeren</button></div>
<div class="cart-list" id="cart-list"></div>
<div class="cart-foot" id="cart-foot"></div>
</div>
</div>
<div class="overlay" id="overlay"></div>
<div class="toast" id="toast"></div>
</div>
<script>
"use strict";
/* =====================================================================
GELD DES SPIELERS (später aus Bank/Item-System)
===================================================================== */
/* Geld und Katalog kommen vom Server. Preise werden beim Kauf
serverseitig neu berechnet — die Anzeige hier ist nur Darstellung. */
const spieler = { bargeld: 0, konto: 0 };
/* =====================================================================
PRODUKT-REGISTRY — alle Waren (id -> Produkt). bestand = Lagerbestand.
rabatt (0..1) macht ein Angebot; lizenz kennzeichnet z. B. Waffenschein.
===================================================================== */
/* Produkt-Registry. In FiveM wird sie beim Öffnen vom Server ersetzt. */
let produkte = {
// --- 24/7 ---
wasser: { name: "Wasserflasche", kat: "Getränke", icon: "🥤", preis: 4, gewicht: 0.5, bestand: 40, beschreibung: "Löscht den Durst." },
cola: { name: "Cola", kat: "Getränke", icon: "🥫", preis: 6, gewicht: 0.4, bestand: 30, beschreibung: "Eiskalt, koffeinhaltig." },
energy: { name: "Energydrink", kat: "Getränke", icon: "⚡", preis: 9, gewicht: 0.4, bestand: 24, beschreibung: "Gibt Extra-Ausdauer." },
kaffee: { name: "Kaffee to go", kat: "Getränke", icon: "☕", preis: 5, gewicht: 0.3, bestand: 18, beschreibung: "Wach werden." },
sandwich: { name: "Sandwich", kat: "Snacks", icon: "🥪", preis: 8, gewicht: 0.2, bestand: 25, beschreibung: "Stillt den Hunger." },
chips: { name: "Chips", kat: "Snacks", icon: "🍟", preis: 5, gewicht: 0.2, bestand: 32, beschreibung: "Knuspriger Snack." },
schoko: { name: "Schokoriegel", kat: "Snacks", icon: "🍫", preis: 3, gewicht: 0.1, bestand: 50, beschreibung: "Süße Energie." },
burger: { name: "Burger", kat: "Snacks", icon: "🍔", preis: 12, gewicht: 0.3, bestand: 15, rabatt: 0.25, beschreibung: "Deftige Mahlzeit." },
verband: { name: "Verband", kat: "Gesundheit", icon: "🩹", preis: 15, gewicht: 0.1, bestand: 20, beschreibung: "Stoppt Blutungen." },
tablette: { name: "Schmerztabletten", kat: "Gesundheit", icon: "💊", preis: 20, gewicht: 0.02, bestand: 16, beschreibung: "Lindert Schmerzen." },
medkit: { name: "Erste-Hilfe-Kit", kat: "Gesundheit", icon: "🧳", preis: 120, gewicht: 1.5, bestand: 8, beschreibung: "Behandelt schwere Wunden." },
repkit: { name: "Reparaturkit", kat: "Werkzeug", icon: "🧰", preis: 250, gewicht: 2.0, bestand: 10, beschreibung: "Repariert Fahrzeuge." },
dietrich: { name: "Dietrich", kat: "Werkzeug", icon: "🗝️", preis: 45, gewicht: 0.05, bestand: 22, beschreibung: "Öffnet einfache Schlösser." },
kabel: { name: "Kabelbinder", kat: "Werkzeug", icon: "🔗", preis: 30, gewicht: 0.1, bestand: 40, beschreibung: "Vielseitig einsetzbar." },
lampe: { name: "Taschenlampe", kat: "Werkzeug", icon: "🔦", preis: 35, gewicht: 0.3, bestand: 14, beschreibung: "Leuchtet den Weg." },
handy: { name: "Smartphone", kat: "Elektronik", icon: "📱", preis: 450, gewicht: 0.3, bestand: 6, beschreibung: "Modernes Handy (Item)." },
sim: { name: "SIM-Karte", kat: "Elektronik", icon: "📶", preis: 60, gewicht: 0, bestand: 30, beschreibung: "Fürs Handy nötig." },
akku: { name: "Akku", kat: "Elektronik", icon: "🔋", preis: 40, gewicht: 0.1, bestand: 45, beschreibung: "Ersatz-Akku fürs Handy." },
funk: { name: "Funkgerät", kat: "Elektronik", icon: "📻", preis: 180, gewicht: 0.4, bestand: 9, beschreibung: "Kurzstrecken-Funk." },
boerse: { name: "Börse", kat: "Zubehör", icon: "👛", preis: 90, gewicht: 0.2, bestand: 12, beschreibung: "Fasst bis zu 10 Dokumente." },
rucksack: { name: "Rucksack", kat: "Zubehör", icon: "🎒", preis: 300, gewicht: 0.6, bestand: 7, rabatt: 0.15, beschreibung: "+ Tragekapazität." },
// --- Ammu-Nation ---
w_pistole: { name: "Pistole P99", kat: "Waffen", icon: "🔫", preis: 3500, gewicht: 1.2, bestand: 5, lizenz: true, beschreibung: "9mm Halbautomatik." },
w_smg: { name: "MP5 (MPX)", kat: "Waffen", icon: "🔫", preis: 8500, gewicht: 3.0, bestand: 3, lizenz: true, beschreibung: "Maschinenpistole." },
w_flinte: { name: "Schrotflinte 870", kat: "Waffen", icon: "🔫", preis: 5200, gewicht: 3.6, bestand: 4, lizenz: true, beschreibung: "Pump-Action Shotgun." },
a_9mm: { name: "9mm Munition (×30)", kat: "Munition", icon: "🧨", preis: 120, gewicht: 0.3, bestand: 60, beschreibung: "Patronen für 9mm." },
a_12: { name: "Schrot 12/70 (×10)", kat: "Munition", icon: "🧨", preis: 90, gewicht: 0.4, bestand: 40, beschreibung: "Schrotpatronen." },
at_sd: { name: "Schalldämpfer", kat: "Aufsätze", icon: "🔇", preis: 900, gewicht: 0.2, bestand: 10, lizenz: true, beschreibung: "Dämpft den Mündungsknall." },
at_lampe: { name: "Waffenlampe", kat: "Aufsätze", icon: "🔦", preis: 400, gewicht: 0.15, bestand: 12, beschreibung: "Taktische Beleuchtung." },
at_laser: { name: "Laservisier", kat: "Aufsätze", icon: "🎯", preis: 650, gewicht: 0.15, bestand: 8, beschreibung: "Bessere Zielerfassung." },
s_weste: { name: "Schutzweste", kat: "Schutz", icon: "🦺", preis: 1200, gewicht: 3.5, bestand: 6, beschreibung: "Reduziert Schaden." },
s_helm: { name: "Ballistikhelm", kat: "Schutz", icon: "⛑️", preis: 800, gewicht: 1.0, bestand: 8, beschreibung: "Schützt den Kopf." }
};
/* =====================================================================
SHOPS — jeder Laden nutzt dieselbe UI, listet nur andere Produkte.
===================================================================== */
/* Läden. In FiveM enthält die Liste nur den gerade betretenen Laden —
man kann im Spiel nicht zwischen Filialen umschalten. */
let shops = [
{ name: "LS 24/7", logo: "🛒", tl: "Alles für den Alltag", accent: "#4ade80", ink: "#06210f",
warnung: null,
produkte: ["wasser","cola","energy","kaffee","sandwich","chips","schoko","burger",
"verband","tablette","medkit","repkit","dietrich","kabel","lampe",
"handy","sim","akku","funk","boerse","rucksack"] },
{ name: "Ammu-Nation", logo: "🔫", tl: "Waffen &amp; Ausrüstung", accent: "#f0684a", ink: "#2a0c05",
warnung: "Für Waffen &amp; bestimmte Aufsätze ist ein gültiger Waffenschein erforderlich.",
produkte: ["w_pistole","w_smg","w_flinte","a_9mm","a_12","at_sd","at_lampe","at_laser","s_weste","s_helm"] }
];
let aktiverShop = 0;
let aktiveKat = "Alle";
let suche = "";
const warenkorb = {}; // id -> Menge
let zahlung = "bargeld"; // "bargeld" | "karte"
/* =====================================================================
HILFEN
===================================================================== */
const $ = id => document.getElementById(id);
const euro = n => n.toLocaleString("de-DE") + " €";
function sicher(t) { const d = document.createElement("div"); d.textContent = t; return d.innerHTML; }
let toastTimer = null;
function toast(text, warn) { const t = $("toast"); t.textContent = text; t.className = "toast show" + (warn ? " warn" : ""); clearTimeout(toastTimer); toastTimer = setTimeout(() => t.classList.remove("show"), 2200); }
// effektiver Preis (mit Rabatt)
const preisVon = p => Math.round(p.preis * (1 - (p.rabatt || 0)));
const shop = () => shops[aktiverShop];
// verfügbarer Bestand abzüglich Warenkorb
const verfuegbar = id => produkte[id].bestand - (warenkorb[id] || 0);
/* =====================================================================
SHOP-WECHSEL & AKZENT
===================================================================== */
function renderShopSwitch() {
$("shop-switch").innerHTML = shops.map((s, i) =>
'<button class="' + (i === aktiverShop ? "active" : "") + '" data-s="' + i + '">' + s.name + "</button>").join("");
$("shop-switch").querySelectorAll("[data-s]").forEach(b => b.addEventListener("click", () => {
if (+b.dataset.s === aktiverShop) return;
aktiverShop = +b.dataset.s; aktiveKat = "Alle"; suche = ""; $("search").value = "";
Object.keys(warenkorb).forEach(k => delete warenkorb[k]); // Warenkorb beim Ladenwechsel leeren
ladeShop();
}));
}
function ladeShop() {
const s = shop();
document.documentElement.style.setProperty("--accent", s.accent);
document.documentElement.style.setProperty("--accent-ink", s.ink);
document.documentElement.style.setProperty("--accent-soft", "color-mix(in srgb, " + s.accent + " 16%, transparent)");
$("shop-name").textContent = s.name;
$("shop-tl").innerHTML = s.tl;
$("shop-logo").textContent = s.logo;
renderShopSwitch();
renderKategorien();
renderProdukte();
renderWarenkorb();
if (s.warnung) toast("ℹ️ " + s.warnung.replace(/&amp;/g, "&"));
}
/* =====================================================================
KATEGORIEN
===================================================================== */
const katIcons = { Alle: "🛍️", Getränke: "🥤", Snacks: "🍔", Gesundheit: "➕", Werkzeug: "🧰", Elektronik: "📱", Zubehör: "🎒",
Waffen: "🔫", Munition: "🧨", Aufsätze: "🎯", Schutz: "🦺" };
function renderKategorien() {
const ids = shop().produkte;
const kats = ["Alle", ...[...new Set(ids.map(id => produkte[id].kat))]];
$("cats").innerHTML = kats.map(k => {
const anzahl = k === "Alle" ? ids.length : ids.filter(id => produkte[id].kat === k).length;
return '<button class="' + (k === aktiveKat ? "active" : "") + '" data-k="' + k + '">' +
'<span class="ci">' + (katIcons[k] || "•") + '</span>' + k + '<span class="cc">' + anzahl + "</span></button>";
}).join("");
$("cats").querySelectorAll("[data-k]").forEach(b => b.addEventListener("click", () => { aktiveKat = b.dataset.k; renderKategorien(); renderProdukte(); }));
}
/* =====================================================================
PRODUKTE
===================================================================== */
function renderProdukte() {
const ids = shop().produkte.filter(id => {
const p = produkte[id];
const katOk = aktiveKat === "Alle" || p.kat === aktiveKat;
const suchOk = !suche || p.name.toLowerCase().includes(suche);
return katOk && suchOk;
});
const grid = $("prod-grid");
if (ids.length === 0) { grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:var(--muted);padding:50px">Keine Produkte gefunden.</div>'; return; }
grid.innerHTML = ids.map(id => {
const p = produkte[id];
const preis = preisVon(p);
const imKorb = warenkorb[id] || 0;
const aus = verfuegbar(id) <= 0;
return '<div class="prod">' +
(p.rabatt ? '<span class="badge-sale">-' + Math.round(p.rabatt * 100) + "%</span>" : "") +
(p.lizenz ? '<span class="badge-lic">🪪 Schein</span>' : "") +
(imKorb ? '<span class="incart">' + imKorb + "</span>" : "") +
'<div class="picon">' + p.icon + "</div>" +
'<div class="pname">' + sicher(p.name) + "</div>" +
'<div class="pdesc">' + sicher(p.beschreibung) + "</div>" +
'<div class="pmeta"><div class="price">' + (p.rabatt ? "<s>" + euro(p.preis) + "</s>" : "") + euro(preis) + "</div>" +
'<div class="stock' + (aus ? " out" : "") + '">' + (aus ? "ausverkauft" : verfuegbar(id) + " Stk.") + "</div></div>" +
'<button class="add" data-add="' + id + '"' + (aus ? " disabled" : "") + ">+ In den Korb</button>" +
"</div>";
}).join("");
grid.querySelectorAll("[data-add]").forEach(b => b.addEventListener("click", () => hinzufuegen(b.dataset.add)));
}
/* =====================================================================
WARENKORB
===================================================================== */
function hinzufuegen(id) {
if (verfuegbar(id) <= 0) { toast("Ausverkauft", true); return; }
warenkorb[id] = (warenkorb[id] || 0) + 1;
renderProdukte(); renderWarenkorb();
}
function aendern(id, delta) {
const neu = (warenkorb[id] || 0) + delta;
if (delta > 0 && verfuegbar(id) <= 0) { toast("Kein Bestand mehr", true); return; }
if (neu <= 0) delete warenkorb[id]; else warenkorb[id] = neu;
renderProdukte(); renderWarenkorb();
}
function warenkorbGesamt() { return Object.keys(warenkorb).reduce((s, id) => s + preisVon(produkte[id]) * warenkorb[id], 0); }
function warenkorbAnzahl() { return Object.values(warenkorb).reduce((s, n) => s + n, 0); }
function renderWarenkorb() {
const ids = Object.keys(warenkorb);
$("cart-count").textContent = ids.length ? "(" + warenkorbAnzahl() + ")" : "";
const list = $("cart-list");
if (ids.length === 0) {
list.innerHTML = '<div class="cart-empty"><div class="big">🛒</div><div>Dein Warenkorb ist leer.</div></div>';
} else {
list.innerHTML = ids.map(id => {
const p = produkte[id];
return '<div class="citem"><span class="ci">' + p.icon + "</span>" +
'<div class="cg"><div class="cn">' + sicher(p.name) + '</div><div class="cp">' + euro(preisVon(p)) + " × " + warenkorb[id] + " = " + euro(preisVon(p) * warenkorb[id]) + "</div></div>" +
'<div class="stepper"><button data-dec="' + id + '">−</button><span class="q">' + warenkorb[id] + '</span><button data-inc="' + id + '">+</button></div></div>';
}).join("");
list.querySelectorAll("[data-dec]").forEach(b => b.addEventListener("click", () => aendern(b.dataset.dec, -1)));
list.querySelectorAll("[data-inc]").forEach(b => b.addEventListener("click", () => aendern(b.dataset.inc, +1)));
}
const gesamt = warenkorbGesamt();
$("cart-foot").innerHTML =
'<div class="sumrow"><span>Artikel</span><span class="v">' + warenkorbAnzahl() + "</span></div>" +
'<div class="sumrow total"><span>Gesamt</span><span class="v">' + euro(gesamt) + "</span></div>" +
'<div class="pay" id="pay">' +
'<button data-p="bargeld" class="' + (zahlung === "bargeld" ? "active" : "") + '">💵 Bargeld</button>' +
'<button data-p="karte" class="' + (zahlung === "karte" ? "active" : "") + '">💳 Karte</button>' +
"</div>" +
'<button class="checkout" id="checkout"' + (gesamt === 0 ? " disabled" : "") + '><span>Bezahlen</span><span>' + euro(gesamt) + "</span></button>";
$("pay").querySelectorAll("[data-p]").forEach(b => b.addEventListener("click", () => { zahlung = b.dataset.p; renderWarenkorb(); }));
if ($("checkout")) $("checkout").addEventListener("click", bezahlen);
aktualisiereGeld();
}
function aktualisiereGeld() {
$("bal-cash").textContent = euro(spieler.bargeld);
$("bal-bank").textContent = euro(spieler.konto);
}
/* =====================================================================
BEZAHLEN
===================================================================== */
function bezahlen() {
const gesamt = warenkorbGesamt();
if (gesamt === 0) return;
if (!IST_FIVEM) {
// Browser-Ansicht: lokal abrechnen, damit das Layout prüfbar bleibt
const verfuegbaresGeld = zahlung === "bargeld" ? spieler.bargeld : spieler.konto;
if (gesamt > verfuegbaresGeld) { toast(zahlung === "bargeld" ? "Nicht genug Bargeld" : "Konto nicht gedeckt", true); return; }
const posten = Object.keys(warenkorb).map(id => ({ name: produkte[id].name, menge: warenkorb[id], summe: preisVon(produkte[id]) * warenkorb[id] }));
if (zahlung === "bargeld") spieler.bargeld -= gesamt; else spieler.konto -= gesamt;
Object.keys(warenkorb).forEach(id => { produkte[id].bestand -= warenkorb[id]; delete warenkorb[id]; });
zeigeBeleg(posten, gesamt);
renderProdukte(); renderWarenkorb();
return;
}
/* In FiveM entscheidet der Server: Er rechnet den Warenkorb neu durch,
prüft Preise, Bestand, Waffenschein, Deckung und Traglast. */
nui("kaufen", { warenkorb: warenkorb, zahlung: zahlung });
}
// Kassenbeleg anzeigen
function zeigeBeleg(posten, gesamt) {
const jetzt = new Date();
const zeit = jetzt.toLocaleDateString("de-DE") + " " + String(jetzt.getHours()).padStart(2, "0") + ":" + String(jetzt.getMinutes()).padStart(2, "0");
$("overlay").innerHTML =
'<div class="receipt"><h3>' + shop().name.toUpperCase() + '</h3><div class="rsub">Los Santos · ' + zeit + "</div>" +
posten.map(p => '<div class="rline"><span>' + p.menge + "× " + sicher(p.name) + '</span><span>' + euro(p.summe) + "</span></div>").join("") +
'<div class="rdiv"></div>' +
'<div class="rtotal"><span>SUMME</span><span>' + euro(gesamt) + "</span></div>" +
'<div class="rline" style="margin-top:6px"><span>Zahlart</span><span>' + (zahlung === "bargeld" ? "Bargeld" : "Karte") + "</span></div>" +
'<div class="rthanks">Vielen Dank für deinen Einkauf!</div>' +
'<button class="rclose" id="r-close">Schließen</button></div>';
$("overlay").classList.add("show");
$("r-close").addEventListener("click", () => $("overlay").classList.remove("show"));
toast("Kauf abgeschlossen ✅");
}
/* =====================================================================
EVENTS & START
===================================================================== */
$("search").addEventListener("input", e => { suche = e.target.value.trim().toLowerCase(); renderProdukte(); });
$("cart-clear").addEventListener("click", () => { Object.keys(warenkorb).forEach(k => delete warenkorb[k]); renderProdukte(); renderWarenkorb(); });
$("overlay").addEventListener("click", e => { if (e.target === $("overlay")) $("overlay").classList.remove("show"); });
/* =====================================================================
NUI-BRÜCKE — Verbindung zum FiveM-Client
===================================================================== */
const IST_FIVEM = (typeof GetParentResourceName === "function");
const RESSOURCE = IST_FIVEM ? GetParentResourceName() : "aether-shop";
/* Schickt eine Aktion an den Client; der reicht sie an den Server weiter. */
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 den Katalog eines Ladens vom Server */
function setzeKatalog(katalog) {
if (!katalog) return;
produkte = katalog.produkte || {};
// Im Spiel gibt es immer nur den betretenen Laden
shops = [{
name: katalog.name, logo: katalog.logo, tl: katalog.tl,
accent: katalog.accent, ink: katalog.ink, warnung: katalog.warnung,
produkte: katalog.reihenfolge || Object.keys(produkte),
}];
aktiverShop = 0;
}
/* Geldanzeige aktualisieren */
function setzeGeld(geld) {
if (!geld) return;
spieler.bargeld = geld.bargeld || 0;
spieler.konto = geld.konto || 0;
}
window.addEventListener("message", (ereignis) => {
const n = ereignis.data || {};
if (n.aktion === "oeffnen") {
document.body.style.display = "";
Object.keys(warenkorb).forEach(k => delete warenkorb[k]);
setzeKatalog(n.katalog);
setzeGeld(n.geld);
ladeShop();
}
else if (n.aktion === "gekauft") {
// Beleg zeigen und Katalog/Geld auffrischen
setzeKatalog(n.katalog);
setzeGeld(n.geld);
Object.keys(warenkorb).forEach(k => delete warenkorb[k]);
zeigeBeleg(n.posten || [], n.gesamt || 0);
renderProdukte(); renderWarenkorb();
}
else if (n.aktion === "fehler") {
toast(n.text || "Kauf nicht möglich", true);
}
else if (n.aktion === "schliessen") {
document.body.style.display = "none";
$("overlay").classList.remove("show");
}
});
/* ESC schließt den Laden */
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 {
// Eigenständige Browser-Ansicht mit Beispieldaten
spieler.bargeld = 860;
spieler.konto = 48320;
ladeShop();
}
</script>
</body>
</html>