- 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>
541 lines
33 KiB
HTML
541 lines
33 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 — 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 & Ausrüstung", accent: "#f0684a", ink: "#2a0c05",
|
||
warnung: "Für Waffen & 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(/&/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>
|