
Exception: Malformed HTML content: <!DOCTYPE html>
<html lang="de">
<head>
<base target="_top">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>EG Webstore</title>
<style>
:root {
--bg: #0f0c13;
--card: #1a1625;
--panel: #13101b;
--line: #332d41;
--primary: #9d4edd;
--accent: #3c096c;
--text: #e0e0e0;
--muted: #9a91aa;
--danger: #9b2226;
--success: #2d6a4f;
--warning: #ffcc00;
--shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family: "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
padding: 12px;
}
.app {
width: min(1100px, 100%);
margin: 0 auto;
}
.card {
background: var(--card);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
padding: 20px;
}
#authArea {
max-width: 440px;
margin: 8vh auto 0;
}
#dashboard { display: none; }
h1 {
color: var(--primary);
font-size: 24px;
margin: 0 0 6px;
text-align: center;
}
h2 {
color: var(--primary);
font-size: 13px;
letter-spacing: 0;
margin: 18px 0 8px;
text-transform: uppercase;
}
h3 {
margin: 0;
font-size: 16px;
line-height: 1.25;
}
p { margin: 0; }
input,
select,
textarea {
width: 100%;
margin: 8px 0;
padding: 12px;
border: 1px solid var(--line);
border-radius: 8px;
outline: none;
background: var(--bg);
color: var(--text);
font: inherit;
}
textarea {
min-height: 96px;
resize: vertical;
}
input:focus,
select:focus,
textarea:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(157, 78, 221, 0.18);
}
button {
min-height: 40px;
padding: 9px 12px;
border: 0;
border-radius: 8px;
cursor: pointer;
font: inherit;
font-weight: 700;
transition: opacity 160ms ease, transform 160ms ease;
}
button:hover:not(:disabled) { transform: translateY(-1px); }
button:disabled { cursor: progress; opacity: 0.55; }
.primary { background: var(--primary); color: white; }
.muted { background: var(--line); color: var(--text); }
.danger { background: var(--danger); color: white; }
.success { background: var(--success); color: white; }
.button-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 10px;
}
.button-row button { flex: 1 1 160px; }
.subtle,
.meta {
color: var(--muted);
font-size: 12px;
line-height: 1.45;
white-space: pre-wrap;
}
#message,
#authMessage {
min-height: 18px;
color: var(--muted);
font-size: 12px;
text-align: right;
white-space: pre-wrap;
}
#authMessage {
color: #ff9ca7;
text-align: center;
}
#message.error { color: #ff9ca7; }
#message.info { color: var(--primary); }
#message.success { color: #74c69d; }
.topbar {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
margin-bottom: 16px;
}
.top-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
flex-wrap: wrap;
}
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(300px, 360px);
gap: 14px;
align-items: start;
}
.toolbar {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(150px, 220px) minmax(150px, 220px);
gap: 8px;
margin: 10px 0;
}
.toolbar input,
.toolbar select { margin: 0; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 10px;
}
.product,
.cart,
.panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
padding: 12px;
}
.product {
position: relative;
display: grid;
gap: 8px;
min-width: 0;
}
.product img,
.placeholder {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
border-radius: 8px;
background: #2a2635;
display: block;
}
.placeholder {
display: grid;
place-items: center;
color: var(--muted);
font-size: 12px;
}
.badge {
position: absolute;
top: 18px;
left: 18px;
border-radius: 4px;
padding: 3px 7px;
background: var(--primary);
color: white;
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
}
.price {
color: var(--primary);
font-size: 18px;
font-weight: 800;
}
.request-status {
color: #74c69d;
font-weight: 700;
font-size: 13px;
}
.cart-item {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid var(--line);
}
.cart-item:last-child { border-bottom: 0; }
.cart-controls {
display: flex;
gap: 6px;
align-items: center;
}
.cart-controls button {
min-height: 32px;
padding: 5px 9px;
}
.summary-line {
display: flex;
justify-content: space-between;
gap: 12px;
margin-top: 8px;
}
.summary-line.total {
color: var(--primary);
font-size: 18px;
font-weight: 800;
border-top: 1px solid var(--line);
padding-top: 10px;
margin-top: 10px;
}
.empty {
margin: 0;
padding: 14px;
text-align: center;
color: var(--muted);
}
.closed {
display: none;
margin: 12px 0;
padding: 20px;
border: 1px solid var(--primary);
border-radius: 8px;
background: rgba(60, 9, 108, 0.32);
text-align: center;
}
.closed.active { display: block; }
.webhook-box {
display: none;
margin-top: 10px;
padding: 12px;
border-radius: 8px;
border: 1px solid var(--line);
background: var(--panel);
color: var(--muted);
font-size: 12px;
line-height: 1.45;
white-space: pre-wrap;
}
.webhook-box.success {
display: block;
border-color: var(--success);
color: #74c69d;
}
.webhook-box.error {
display: block;
border-color: var(--danger);
color: #ff9ca7;
}
.webhook-box.info {
display: block;
border-color: var(--primary);
color: var(--primary);
}
#plotIdWrapper,
#giftFields { display: none; }
.gift-toggle {
display: flex;
align-items: center;
gap: 8px;
color: var(--muted);
font-size: 13px;
margin: 8px 0;
}
.gift-toggle input {
width: auto;
margin: 0;
}
@media (max-width: 840px) {
.layout { grid-template-columns: 1fr; }
.toolbar { grid-template-columns: 1fr; }
.topbar { display: block; }
.top-actions { justify-content: flex-start; margin-top: 10px; }
#message { text-align: left; margin-top: 8px; }
}
</style>
</head>
<body>
<main class="app">
<section id="authArea" class="card">
<h1>EG Webstore</h1>
<p class="subtle" style="text-align:center;">Login mit deinem normalen Account. Anfragen werden automatisch deinem Spielernamen zugeordnet.</p>
<input id="usernameInput" type="text" autocomplete="username" placeholder="Name">
<input id="passwordInput" type="password" autocomplete="current-password" placeholder="Passwort">
<div class="button-row">
<button id="loginButton" class="primary" type="button">Login</button>
<button id="registerButton" class="muted" type="button">Registrieren</button>
</div>
<p id="authMessage"></p>
</section>
<section id="dashboard" class="card">
<div class="topbar">
<div>
<h1 style="text-align:left;">EG Webstore</h1>
<p id="userInfo" class="subtle"></p>
</div>
<div>
<div class="top-actions">
<button id="refreshButton" class="muted" type="button">Aktualisieren</button>
<button id="logoutButton" class="muted" type="button">Logout</button>
</div>
<div id="message"></div>
</div>
</div>
<p class="subtle">
Alle Preise beziehen sich auf die Ingame-Waehrung des JDBROS-Servers. Das Symbol $ steht fuer Servergeld und nicht fuer Echtgeld.
</p>
<div id="shopClosedBox" class="closed"></div>
<div class="layout">
<div>
<h2>Produkte auf Anfrage</h2>
<div class="toolbar">
<input id="productSearch" type="search" placeholder="Produkte suchen...">
<select id="categoryFilter">
<option value="">Alle Kategorien</option>
</select>
<select id="sortSelect">
<option value="default">Sortierung</option>
<option value="priceAsc">Preis aufsteigend</option>
<option value="priceDesc">Preis absteigend</option>
<option value="nameAsc">Name A-Z</option>
<option value="nameDesc">Name Z-A</option>
</select>
</div>
<div id="productGrid" class="grid"></div>
</div>
<aside class="cart">
<h2>Anfragekorb</h2>
<div id="cartItems"></div>
<div class="panel" style="margin-top:10px;">
<input id="discountCode" type="text" placeholder="Rabattcode">
<button id="discountButton" class="muted" type="button" style="width:100%;">Rabatt pruefen</button>
<div id="discountInfo" class="meta"></div>
</div>
<div class="summary-line"><span>Zwischensumme</span><strong id="subtotalPrice">0,00 $</strong></div>
<div class="summary-line"><span>Aufpreis</span><strong id="surchargePrice">0,00 $</strong></div>
<div class="summary-line"><span>Rabatt</span><strong id="discountPrice">-0,00 $</strong></div>
<div class="summary-line total"><span>Gesamt</span><strong id="cartTotal">0,00 $</strong></div>
<button id="clearCartButton" class="danger" type="button" style="width:100%; margin-top:12px;">Anfragekorb leeren</button>
<h2>Anfrage senden</h2>
<form id="orderForm">
<input id="discordName" type="text" placeholder="Discord Name" required>
<select id="deliveryType" required>
<option value="">Lieferart waehlen</option>
<option value="Abholung">Abholung</option>
<option value="Lieferung zum Plot">Lieferung zum Plot</option>
</select>
<div id="plotIdWrapper">
<input id="plotId" type="text" placeholder="Plot ID">
</div>
<textarea id="customerNote" placeholder="Nachricht / Hinweis zur Anfrage"></textarea>
<label class="gift-toggle">
<input id="isGift" type="checkbox">
Als Geschenk anfragen
</label>
<div id="giftFields">
<input id="giftRecipientName" type="text" placeholder="Empfaenger Ingame Name">
<textarea id="giftMessage" placeholder="Geschenk Nachricht"></textarea>
</div>
<div id="orderPreview" class="panel meta">Anfragevorschau erscheint hier.</div>
<button id="submitOrderButton" class="primary" type="submit" style="width:100%;">Anfrage senden</button>
</form>
<div id="discordFeedback" class="webhook-box"></div>
<h2>Anfrage verfolgen</h2>
<button id="trackOrderButton" class="muted" type="button" style="width:100%;">Meine offene Anfrage pruefen</button>
<div id="trackingResult" class="meta"></div>
</aside>
</div>
</section>
</main>
<script>
const PRODUCT_SURCHARGE = 500;
const state = {
user: "",
token: "",
trust: {},
products: [],
categories: [],
cart: [],
discount: null,
shopClosed: false,
surcharge: PRODUCT_SURCHARGE
};
let locked = false;
const $ = id => document.getElementById(id);
function setLocked(value) {
locked = value;
document.querySelectorAll("button, input, select, textarea").forEach(control => {
control.disabled = value;
});
if (value) showMessage("Synchronisiere...", "info", true);
if (!value && $("message").textContent === "Synchronisiere...") showMessage("");
}
function showMessage(text, type = "", sticky = false) {
const target = $("dashboard").style.display === "block" ? $("message") : $("authMessage");
target.textContent = text || "";
target.className = type || "";
if (text && !sticky && type !== "error") {
window.setTimeout(() => {
if (!locked && target.textContent === text) target.textContent = "";
}, 2600);
}
}
function showDiscordFeedback(webhook) {
const box = $("discordFeedback");
webhook = webhook || {};
box.className = "webhook-box";
box.textContent = "";
if (!webhook.status) return;
if (webhook.status === "success") {
box.classList.add("success");
box.textContent = "Discord: Nachricht wurde erfolgreich gesendet.";
return;
}
if (webhook.status === "skipped") {
box.classList.add("error");
box.textContent =
"Discord: Nachricht wurde NICHT gesendet.\n" +
"Grund: " + (webhook.message || "Webhook wurde uebersprungen.") + "\n" +
"Pruefe im Settings-Sheet den Key webstoreOrderWebhookUrl.";
return;
}
box.classList.add("error");
box.textContent =
"Discord: Nachricht wurde NICHT gesendet.\n" +
"Status: " + (webhook.status || "-") + "\n" +
(webhook.code ? "HTTP-Code: " + webhook.code + "\n" : "") +
(webhook.message ? "Meldung: " + webhook.message + "\n" : "") +
(webhook.body ? "Antwort: " + webhook.body : "");
}
function serverCall(method, args = []) {
return new Promise((resolve, reject) => {
if (!window.google || !google.script || !google.script.run) {
reject(new Error("Google Apps Script ist lokal nicht verfuegbar."));
return;
}
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(reject)[method](...args);
});
}
async function call(method, args = []) {
setLocked(true);
try {
const res = await serverCall(method, args);
if (!res || !res.ok) {
if (res && res.error) throw new Error(res.error);
let details = "";
try {
details = JSON.stringify(res);
} catch (err) {
details = String(res);
}
throw new Error("Unerwartete Server-Antwort von " + method + ": " + details);
}
return res;
} finally {
setLocked(false);
}
}
async function authenticate(mode) {
const username = $("usernameInput").value.trim();
const password = $("passwordInput").value;
if (!username || !password) {
showMessage("Bitte Name und Passwort eingeben.", "error");
return;
}
try {
const auth = await call("getInitialData", [username, password, mode]);
state.token = auth.token;
const webstore = await call("getWebstoreInitialData", [state.token]);
applyStore(webstore);
$("authArea").style.display = "none";
$("dashboard").style.display = "block";
} catch (err) {
showMessage(err.message || String(err), "error");
}
}
async function refreshStore() {
try {
const data = await call("getWebstoreInitialData", [state.token]);
applyStore(data);
showMessage("Aktualisiert.", "info");
} catch (err) {
showMessage(err.message || String(err), "error");
}
}
function applyStore(data) {
state.user = data.user || state.user;
state.token = data.token || state.token;
state.trust = data.trust || {};
state.products = Array.isArray(data.products) ? data.products : [];
state.categories = Array.isArray(data.categories) ? data.categories : [];
state.surcharge = Number(data.surcharge || PRODUCT_SURCHARGE);
state.shopClosed = Boolean(data.shopStatus && data.shopStatus.closed);
$("userInfo").textContent =
`${state.user} | Vertrauen: ${(state.trust && state.trust.level) || "Neu"} | ${(state.trust && state.trust.shopOrders) || 0} Shop-Anfragen`;
renderCategories();
renderShopStatus(data.shopStatus || {});
renderProducts();
renderCart();
renderTracking(data.activeOrder || null);
}
function renderShopStatus(status) {
if (state.shopClosed) {
$("shopClosedBox").classList.add("active");
$("shopClosedBox").textContent = status.reason || "Wegen Wartungsarbeiten voruebergehend geschlossen";
} else {
$("shopClosedBox").classList.remove("active");
$("shopClosedBox").textContent = "";
}
}
function renderCategories() {
$("categoryFilter").innerHTML =
'<option value="">Alle Kategorien</option>' +
state.categories.map(category =>
`<option value="${escapeHtml(category.name)}">${escapeHtml(category.name)} (${category.count})</option>`
).join("");
}
function getFilteredProducts() {
const query = $("productSearch").value.trim().toLowerCase();
const category = $("categoryFilter").value;
const sort = $("sortSelect").value;
const products = state.products.filter(product => {
const text = `${product.name || ""} ${product.description || ""}`.toLowerCase();
return (!query || text.includes(query)) && (!category || product.category === category);
});
if (sort === "priceAsc") products.sort((a, b) => Number(a.price) - Number(b.price));
else if (sort === "priceDesc") products.sort((a, b) => Number(b.price) - Number(a.price));
else if (sort === "nameAsc") products.sort((a, b) => String(a.name).localeCompare(String(b.name), "de"));
else if (sort === "nameDesc") products.sort((a, b) => String(b.name).localeCompare(String(a.name), "de"));
else products.sort((a, b) => Number(a.sortOrder || 9999) - Number(b.sortOrder || 9999));
return products;
}
function renderProducts() {
const grid = $("productGrid");
if (state.shopClosed) {
grid.innerHTML = "";
return;
}
const products = getFilteredProducts();
if (!products.length) {
grid.innerHTML = '<p class="empty">Keine passenden Produkte gefunden.</p>';
return;
}
grid.innerHTML = products.map(product => {
const image = product.image
? `<img src="${escapeHtml(product.image)}" alt="${escapeHtml(product.name)}" loading="lazy">`
: '<div class="placeholder">Kein Bild</div>';
const badge = product.badge || "AUF ANFRAGE";
const status = product.availabilityText || product.status || "Auf Anfrage";
return `
<article class="product">
<span class="badge">${escapeHtml(badge)}</span>
${image}
<h3>${escapeHtml(product.name)}</h3>
<p class="meta">${escapeHtml(product.description || "")}</p>
<div class="price">${money(product.price)}</div>
<p class="meta">+ ${money(state.surcharge)} Aufpreis pro Stueck</p>
<p class="request-status">${escapeHtml(status)}</p>
<p class="meta">${escapeHtml(product.category || "Allgemein")}</p>
<button class="primary" type="button" data-action="add" data-id="${escapeHtml(product.id)}">Zur Anfrage hinzufuegen</button>
</article>
`;
}).join("");
}
function addToCart(productId) {
if (state.shopClosed) return;
const product = state.products.find(item => String(item.id) === String(productId));
if (!product) return;
const existing = state.cart.find(item => String(item.id) === String(productId));
if (existing) existing.quantity += 1;
else {
state.cart.push({
id: product.id,
name: product.name,
price: Number(product.price) || 0,
quantity: 1
});
}
clearDiscount("Anfragekorb geaendert. Bitte Rabattcode erneut pruefen.");
renderCart();
}
function changeQuantity(productId, delta) {
const item = state.cart.find(entry => String(entry.id) === String(productId));
if (!item) return;
item.quantity += delta;
if (item.quantity <= 0) {
state.cart = state.cart.filter(entry => String(entry.id) !== String(productId));
}
clearDiscount("Anfragekorb geaendert. Bitte Rabattcode erneut pruefen.");
renderCart();
}
function clearCart() {
state.cart = [];
clearDiscount("");
renderCart();
}
function clearDiscount(message) {
state.discount = null;
$("discountCode").value = "";
$("discountInfo").textContent = message || "";
}
function getSubtotal() {
return roundMoney(state.cart.reduce((sum, item) =>
sum + Number(item.price || 0) * Number(item.quantity || 0), 0
));
}
function getItemCount() {
return state.cart.reduce((sum, item) => sum + Number(item.quantity || 0), 0);
}
function getSurcharge() {
return roundMoney(getItemCount() * state.surcharge);
}
function getDiscountAmount() {
if (!state.discount) return 0;
return Math.min(Number(state.discount.discountAmount || 0), getSubtotal());
}
function getTotal() {
return roundMoney(Math.max(0, getSubtotal() + getSurcharge() - getDiscountAmount()));
}
function renderCart() {
if (!state.cart.length) {
$("cartItems").innerHTML = '<p class="empty">Dein Anfragekorb ist leer.</p>';
} else {
$("cartItems").innerHTML = state.cart.map(item => `
<div class="cart-item">
<div>
<strong>${escapeHtml(item.name)}</strong><br>
<span class="meta">${money(item.price)} | Menge: ${item.quantity}</span>
</div>
<div class="cart-controls">
<button type="button" data-action="minus" data-id="${escapeHtml(item.id)}">-</button>
<span>${item.quantity}</span>
<button type="button" data-action="plus" data-id="${escapeHtml(item.id)}">+</button>
</div>
</div>
`).join("");
}
$("subtotalPrice").textContent = money(getSubtotal());
$("surchargePrice").textContent = money(getSurcharge());
$("discountPrice").textContent = "-" + money(getDiscountAmount());
$("cartTotal").textContent = money(getTotal());
renderOrderPreview();
}
async function applyDiscountCode() {
const code = $("discountCode").value.trim();
if (!code) {
$("discountInfo").textContent = "Bitte einen Rabattcode eingeben.";
return;
}
if (!state.cart.length) {
$("discountInfo").textContent = "Bitte zuerst Produkte in den Anfragekorb legen.";
return;
}
try {
const res = await call("validateWebstoreDiscount", [state.token, code, getSubtotal(), state.cart]);
state.discount = res;
$("discountInfo").textContent = `Rabattcode aktiv: ${res.code} (-${money(res.discountAmount)})`;
renderCart();
} catch (err) {
state.discount = null;
$("discountInfo").textContent = err.message || String(err);
renderCart();
}
}
function updateDeliveryFields() {
const needsPlot = $("deliveryType").value === "Lieferung zum Plot";
$("plotIdWrapper").style.display = needsPlot ? "block" : "none";
$("plotId").required = needsPlot;
if (!needsPlot) $("plotId").value = "";
renderOrderPreview();
}
function updateGiftFields() {
const enabled = $("isGift").checked;
$("giftFields").style.display = enabled ? "block" : "none";
$("giftRecipientName").required = enabled;
if (!enabled) {
$("giftRecipientName").value = "";
$("giftMessage").value = "";
}
renderOrderPreview();
}
function renderOrderPreview() {
if (!state.cart.length) {
$("orderPreview").textContent = "Anfragevorschau erscheint hier.";
return;
}
const lines = state.cart.map(item =>
`${item.name} x${item.quantity} = ${money(item.price * item.quantity)}`
);
let text = "Anfragevorschau\n\n" + lines.join("\n") +
"\n\nZwischensumme: " + money(getSubtotal()) +
"\nAufpreis: " + money(getSurcharge()) +
"\nRabatt: -" + money(getDiscountAmount()) +
"\nGesamt: " + money(getTotal()) +
"\nLieferart: " + ($("deliveryType").value || "Nicht gewaehlt");
if ($("deliveryType").value === "Lieferung zum Plot") {
text += "\nPlot ID: " + ($("plotId").value.trim() || "Nicht angegeben");
}
if ($("isGift").checked) {
text += "\nGeschenk fuer: " + ($("giftRecipientName").value.trim() || "Nicht angegeben");
}
$("orderPreview").textContent = text;
}
<script>
async function submitOrder(event) {
event.preventDefault();
if (state.shopClosed) {
showMessage("Der Shop ist aktuell geschlossen.", "error");
return;
}
if (!state.cart.length) {
showMessage("Bitte zuerst Produkte in den Anfragekorb legen.", "error");
return;
}
const payload = {
discordName: $("discordName").value.trim(),
deliveryType: $("deliveryType").value,
plotId: $("plotId").value.trim(),
customerNote: $("customerNote").value.trim(),
items: state.cart,
subtotal: getSubtotal(),
itemCount: getItemCount(),
productSurcharge: getSurcharge(),
discountCode: state.discount ? state.discount.code : "",
discountAmount: state.discount ? state.discount.discountAmount : 0,
total: getTotal(),
isGift: $("isGift").checked,
giftRecipientName: $("giftRecipientName").value.trim(),
giftMessage: $("giftMessage").value.trim()
};
try {
const res = await call("saveWebstoreOrder", [state.token, payload]);
const webhook = res.webhook || {};
state.cart = [];
state.discount = null;
state.products = Array.isArray(res.products) ? res.products : state.products;
state.trust = res.trust || state.trust;
$("orderForm").reset();
updateDeliveryFields();
updateGiftFields();
renderProducts();
renderCart();
renderTracking(res.activeOrder || null);
showDiscordFeedback(webhook);
let discordText = "\nDiscord: Nachricht wurde NICHT gesendet.";
discordText += "\nWebhook-Status: " + (webhook.status || "keine Rueckgabe");
if (webhook.code) discordText += "\nHTTP-Code: " + webhook.code;
if (webhook.message) discordText += "\nMeldung: " + webhook.message;
if (webhook.body) discordText += "\nAntwort: " + webhook.body;
if (webhook.status === "success") {
showMessage(
"Anfrage erfolgreich gesendet.\nBestellnummer: " + res.orderNumber + "\nDiscord: Nachricht wurde gesendet.",
"success",
true
);
} else {
showMessage(
"Anfrage wurde gespeichert.\nBestellnummer: " + res.orderNumber + discordText,
"error",
true
);
}
} catch (err) {
showMessage(err.message || String(err), "error", true);
}
}
</script>
try {
const res = await call("saveWebstoreOrder", [state.token, payload]);
const webhook = res.webhook || {};
state.cart = [];
state.discount = null;
state.products = Array.isArray(res.products) ? res.products : state.products;
state.trust = res.trust || state.trust;
$("orderForm").reset();
updateDeliveryFields();
updateGiftFields();
renderProducts();
renderCart();
renderTracking(res.activeOrder || null);
showDiscordFeedback(webhook);
if (webhook.status === "success") {
showMessage("Anfrage erfolgreich gesendet.\nBestellnummer: " + res.orderNumber + "\nDiscord: Nachricht wurde gesendet.", "success");
} else {
showMessage("Anfrage wurde gespeichert.\nBestellnummer: " + res.orderNumber + "\nDiscord: Nachricht wurde NICHT gesendet.", "error", true);
}
} catch (err) {
showMessage(err.message || String(err), "error");
}
}
async function trackOrder() {
try {
const res = await call("trackWebstoreOrder", [state.token]);
renderTracking(res.order || null);
} catch (err) {
$("trackingResult").textContent = err.message || String(err);
}
}
function renderTracking(order) {
if (!order) {
$("trackingResult").textContent = "Keine offene Anfrage gefunden.";
return;
}
const handler = order.handler || order.worker || order.assignedTo || "";
const response = order.handlerResponse || order.response || order.adminNote || "";
$("trackingResult").textContent =
`Bestellnummer: ${order.orderNumber || "-"}\n` +
`Status: ${order.statusText || "-"}\n` +
`Bearbeiter: ${handler || "Noch nicht zugewiesen"}\n` +
(response ? `Rueckmeldung: ${response}\n` : "") +
`Datum: ${order.date || "-"}\n` +
`Produkte: ${order.products || "-"}\n` +
`Lieferart: ${order.deliveryType || "-"}\n` +
`Gesamt: ${order.total || "0,00 $"}`;
}
async function logout() {
try {
if (state.token) await call("logout", [state.token]);
} catch (err) {
// Reload reicht, falls der Server nicht antwortet.
}
window.location.reload();
}
function money(value) {
return `${roundMoney(value).toLocaleString("de-DE", {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})} $`;
}
function roundMoney(value) {
return Math.round((Number(value) || 0) * 100) / 100;
}
function escapeHtml(value) {
return String(value === undefined || value === null ? "" : value)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
$("loginButton").addEventListener("click", () => authenticate("login"));
$("registerButton").addEventListener("click", () => authenticate("register"));
$("refreshButton").addEventListener("click", refreshStore);
$("logoutButton").addEventListener("click", logout);
$("productSearch").addEventListener("input", renderProducts);
$("categoryFilter").addEventListener("change", renderProducts);
$("sortSelect").addEventListener("change", renderProducts);
$("discountButton").addEventListener("click", applyDiscountCode);
$("clearCartButton").addEventListener("click", clearCart);
$("deliveryType").addEventListener("change", updateDeliveryFields);
$("plotId").addEventListener("input", renderOrderPreview);
$("customerNote").addEventListener("input", renderOrderPreview);
$("isGift").addEventListener("change", updateGiftFields);
$("giftRecipientName").addEventListener("input", renderOrderPreview);
$("giftMessage").addEventListener("input", renderOrderPreview);
$("orderForm").addEventListener("submit", submitOrder);
$("trackOrderButton").addEventListener("click", trackOrder);
$("productGrid").addEventListener("click", event => {
const button = event.target.closest("button[data-action='add']");
if (button) addToCart(button.dataset.id);
});
$("cartItems").addEventListener("click", event => {
const button = event.target.closest("button[data-action]");
if (!button) return;
if (button.dataset.action === "plus") changeQuantity(button.dataset.id, 1);
if (button.dataset.action === "minus") changeQuantity(button.dataset.id, -1);
});
</script>
</body>
</html>. (line 140, file "Code")