diff --git a/app.js b/app.js
index a399f79..ec10ad8 100644
--- a/app.js
+++ b/app.js
@@ -33,6 +33,7 @@ const combineRoutes = require("./routes/combine.route");
const bazaarRoutes = require("./routes/bazaar.route");
const himmelstorRoutes = require("./routes/himmelstor.route");
const himmelstorDailyRoutes = require("./routes/himmelstor-daily.route");
+const gildenhalleRoutes = require("./routes/gildenhalle.route");
const compression = require("compression");
@@ -433,6 +434,7 @@ app.use("/api", combineRoutes);
app.use("/api", bazaarRoutes);
app.use("/himmelstor", himmelstorRoutes);
app.use("/api/himmelstor/daily", himmelstorDailyRoutes);
+app.use("/api", gildenhalleRoutes);
/* ========================
Energie abfragen
diff --git a/public/css/gildenhalle.css b/public/css/gildenhalle.css
new file mode 100644
index 0000000..486123d
--- /dev/null
+++ b/public/css/gildenhalle.css
@@ -0,0 +1,522 @@
+/* ============================================================
+ public/css/gildenhalle.css
+============================================================ */
+
+/* ── Gilden-spezifische Farben ───────────────────────────── */
+:root {
+ --guild-gold: #f0d060;
+ --guild-border: #8b6a3c;
+ --guild-dark: #1a0f04;
+ --guild-mid: #2a1a08;
+ --guild-text: #f0d9a6;
+ --guild-muted: #a08060;
+ --guild-green: #4aaa30;
+ --guild-red: #cc3030;
+}
+
+/* ── Mitglieder-Tabelle ─────────────────────────────────── */
+.gh-members-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-family: "Cinzel", serif;
+ font-size: 12px;
+}
+.gh-members-table th {
+ color: var(--guild-muted);
+ font-size: 10px;
+ letter-spacing: 1px;
+ text-transform: uppercase;
+ padding: 6px 10px;
+ border-bottom: 1px solid var(--guild-border);
+ text-align: left;
+}
+.gh-members-table td {
+ padding: 7px 10px;
+ border-bottom: 1px solid rgba(139,106,42,.2);
+ color: var(--guild-text);
+ vertical-align: middle;
+}
+.gh-members-table tr:hover td {
+ background: rgba(255,255,255,.03);
+}
+
+/* ── Rang-Badge ─────────────────────────────────────────── */
+.gh-rank-badge {
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 10px;
+ font-size: 10px;
+ font-weight: bold;
+ background: linear-gradient(#3a2810, #1a0f04);
+ border: 1px solid var(--guild-border);
+ color: var(--guild-gold);
+ white-space: nowrap;
+}
+.gh-rank-badge.leader {
+ border-color: var(--guild-gold);
+ background: linear-gradient(#6b4b2a, #3c2414);
+ box-shadow: 0 0 6px rgba(200,160,60,.3);
+}
+
+/* ── Gilden-Karten im Suchraster ────────────────────────── */
+.gh-guild-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 10px;
+ padding: 12px 14px;
+ overflow-y: auto;
+ align-content: start;
+}
+
+.gh-guild-card {
+ background: linear-gradient(135deg, var(--guild-mid), var(--guild-dark));
+ border: 2px solid var(--guild-border);
+ border-radius: 10px;
+ padding: 12px 14px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ transition: .2s;
+ cursor: default;
+}
+.gh-guild-card:hover {
+ border-color: var(--guild-gold);
+ box-shadow: 0 4px 16px rgba(0,0,0,.5), 0 0 10px rgba(200,160,60,.15);
+}
+
+.gh-guild-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.gh-guild-tag {
+ background: linear-gradient(#6b4b2a, #3c2414);
+ border: 1px solid var(--guild-gold);
+ border-radius: 6px;
+ padding: 2px 8px;
+ font-family: "Cinzel", serif;
+ font-size: 12px;
+ font-weight: bold;
+ color: var(--guild-gold);
+ flex-shrink: 0;
+}
+.gh-guild-name {
+ font-family: "Cinzel", serif;
+ font-size: 13px;
+ font-weight: bold;
+ color: var(--guild-text);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.gh-guild-desc {
+ font-family: "Cinzel", serif;
+ font-size: 10px;
+ color: var(--guild-muted);
+ line-height: 1.5;
+ flex: 1;
+}
+.gh-guild-meta {
+ display: flex;
+ justify-content: space-between;
+ font-family: "Cinzel", serif;
+ font-size: 10px;
+ color: var(--guild-muted);
+}
+.gh-guild-open {
+ color: var(--guild-green);
+ font-weight: bold;
+}
+.gh-guild-closed {
+ color: #e0a030;
+ font-weight: bold;
+}
+
+.gh-join-btn {
+ margin-top: 4px;
+ padding: 5px 0;
+ background: linear-gradient(#3a2810, #1a0f04);
+ border: 2px solid var(--guild-border);
+ border-radius: 6px;
+ color: var(--guild-text);
+ font-family: "Cinzel", serif;
+ font-size: 11px;
+ font-weight: bold;
+ cursor: pointer;
+ transition: .15s;
+ width: 100%;
+}
+.gh-join-btn:hover:not(:disabled) {
+ border-color: var(--guild-gold);
+ color: #fff;
+}
+.gh-join-btn:disabled {
+ opacity: .45;
+ cursor: not-allowed;
+}
+.gh-join-btn.pending {
+ border-color: #e0a030;
+ color: #e0a030;
+}
+
+/* ── Gilde gründen ──────────────────────────────────────── */
+.gh-create-form {
+ padding: 16px 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ max-width: 440px;
+}
+.gh-form-row {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.gh-form-label {
+ font-family: "Cinzel", serif;
+ font-size: 11px;
+ color: var(--guild-muted);
+ letter-spacing: 1px;
+}
+.gh-form-input,
+.gh-form-textarea,
+.gh-form-select {
+ background: rgba(0,0,0,.5);
+ border: 2px solid var(--guild-border);
+ border-radius: 7px;
+ color: var(--guild-text);
+ font-family: "Cinzel", serif;
+ font-size: 13px;
+ padding: 7px 10px;
+ outline: none;
+ transition: border-color .15s;
+}
+.gh-form-input:focus,
+.gh-form-textarea:focus,
+.gh-form-select:focus { border-color: var(--guild-gold); }
+.gh-form-textarea { resize: vertical; min-height: 70px; }
+.gh-form-select { cursor: pointer; }
+
+.gh-create-btn {
+ padding: 10px 0;
+ background: linear-gradient(#6b4b2a, #3c2414);
+ border: 2px solid var(--guild-gold);
+ border-radius: 8px;
+ color: #fff5cc;
+ font-family: "Cinzel", serif;
+ font-size: 13px;
+ font-weight: bold;
+ cursor: pointer;
+ transition: .2s;
+ letter-spacing: 2px;
+}
+.gh-create-btn:hover {
+ box-shadow: 0 0 14px rgba(200,160,60,.4);
+}
+
+.gh-error-msg {
+ font-family: "Cinzel", serif;
+ font-size: 11px;
+ color: #ff6666;
+ text-align: center;
+}
+
+/* ── Aufgaben ────────────────────────────────────────────── */
+.gh-tasks-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 12px 16px;
+ overflow-y: auto;
+}
+
+.gh-task-card {
+ background: linear-gradient(135deg, var(--guild-mid), var(--guild-dark));
+ border: 2px solid var(--guild-border);
+ border-radius: 10px;
+ padding: 12px 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ transition: .2s;
+}
+.gh-task-card.completed {
+ border-color: var(--guild-green);
+ opacity: .75;
+}
+
+.gh-task-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+.gh-task-label {
+ font-family: "Cinzel", serif;
+ font-size: 13px;
+ color: var(--guild-text);
+ font-weight: bold;
+}
+.gh-task-reward {
+ font-family: "Cinzel", serif;
+ font-size: 11px;
+ color: var(--guild-gold);
+}
+.gh-task-done-badge {
+ font-family: "Cinzel", serif;
+ font-size: 11px;
+ color: var(--guild-green);
+ font-weight: bold;
+}
+
+.gh-task-progress-wrap {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.gh-task-bar-track {
+ height: 8px;
+ background: rgba(255,255,255,.08);
+ border-radius: 4px;
+ overflow: hidden;
+ border: 1px solid rgba(139,106,42,.3);
+}
+.gh-task-bar-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #4a8a3c, #7ada60);
+ border-radius: 4px;
+ transition: width .5s ease;
+}
+.gh-task-bar-fill.completed {
+ background: linear-gradient(90deg, var(--guild-green), #8aff60);
+}
+.gh-task-progress-text {
+ font-family: "Cinzel", serif;
+ font-size: 10px;
+ color: var(--guild-muted);
+ display: flex;
+ justify-content: space-between;
+}
+
+/* ── Anfragen-Liste ─────────────────────────────────────── */
+.gh-requests-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ padding: 8px 0;
+}
+.gh-request-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 12px;
+ background: rgba(0,0,0,.25);
+ border: 1px solid rgba(139,106,42,.25);
+ border-radius: 7px;
+ gap: 10px;
+}
+.gh-request-name {
+ font-family: "Cinzel", serif;
+ font-size: 12px;
+ color: var(--guild-text);
+ flex: 1;
+}
+.gh-request-msg {
+ font-family: "Cinzel", serif;
+ font-size: 10px;
+ color: var(--guild-muted);
+ font-style: italic;
+ flex: 2;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.gh-req-accept,
+.gh-req-reject {
+ padding: 4px 12px;
+ border-radius: 5px;
+ font-family: "Cinzel", serif;
+ font-size: 10px;
+ font-weight: bold;
+ cursor: pointer;
+ transition: .15s;
+ flex-shrink: 0;
+}
+.gh-req-accept {
+ background: linear-gradient(#1a4a18, #0f2a0e);
+ border: 2px solid #4a8a3c;
+ color: #a0e090;
+}
+.gh-req-accept:hover { border-color: #7ada60; }
+.gh-req-reject {
+ background: linear-gradient(#4a1010, #2a0808);
+ border: 2px solid #8a3030;
+ color: #e07070;
+}
+.gh-req-reject:hover { border-color: #ff6060; }
+
+/* ── Meine Gilde Info-Box ───────────────────────────────── */
+.gh-my-info {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ background: rgba(0,0,0,.3);
+ border-bottom: 1px solid var(--guild-border);
+ flex-shrink: 0;
+}
+.gh-my-tag {
+ font-family: "Cinzel", serif;
+ font-size: 20px;
+ font-weight: bold;
+ color: var(--guild-gold);
+ background: linear-gradient(#6b4b2a, #3c2414);
+ border: 2px solid var(--guild-gold);
+ border-radius: 8px;
+ padding: 4px 12px;
+}
+.gh-my-name {
+ font-family: "Cinzel", serif;
+ font-size: 16px;
+ font-weight: bold;
+ color: var(--guild-text);
+}
+.gh-my-sub {
+ font-family: "Cinzel", serif;
+ font-size: 11px;
+ color: var(--guild-muted);
+}
+.gh-leave-btn {
+ margin-left: auto;
+ padding: 6px 16px;
+ background: linear-gradient(#3a1010, #1a0808);
+ border: 2px solid #6a2020;
+ border-radius: 7px;
+ color: #d08080;
+ font-family: "Cinzel", serif;
+ font-size: 11px;
+ cursor: pointer;
+ transition: .15s;
+}
+.gh-leave-btn:hover { border-color: #cc4040; color: #ff9090; }
+
+/* ── Suche ──────────────────────────────────────────────── */
+.gh-search-bar {
+ display: flex;
+ gap: 8px;
+ padding: 10px 14px;
+ border-bottom: 1px solid rgba(139,106,42,.3);
+ flex-shrink: 0;
+}
+.gh-search-input {
+ flex: 1;
+ background: rgba(0,0,0,.5);
+ border: 2px solid var(--guild-border);
+ border-radius: 7px;
+ color: var(--guild-text);
+ font-family: "Cinzel", serif;
+ font-size: 12px;
+ padding: 6px 10px;
+ outline: none;
+}
+.gh-search-input:focus { border-color: var(--guild-gold); }
+.gh-search-btn {
+ padding: 6px 16px;
+ background: linear-gradient(#3a2810, #1a0f04);
+ border: 2px solid var(--guild-border);
+ border-radius: 7px;
+ color: var(--guild-text);
+ font-family: "Cinzel", serif;
+ font-size: 12px;
+ cursor: pointer;
+ transition: .15s;
+}
+.gh-search-btn:hover { border-color: var(--guild-gold); }
+
+/* ── Keine Gilde Hinweis ────────────────────────────────── */
+.gh-no-guild {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ gap: 12px;
+ font-family: "Cinzel", serif;
+ color: var(--guild-muted);
+ text-align: center;
+ padding: 24px;
+}
+.gh-no-guild-icon { font-size: 42px; }
+.gh-no-guild-text { font-size: 13px; line-height: 1.8; }
+
+/* ── Pagination ─────────────────────────────────────────── */
+.gh-pagination {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+ padding: 8px;
+ border-top: 1px solid rgba(139,106,42,.3);
+ flex-shrink: 0;
+}
+.gh-page-btn {
+ background: linear-gradient(#3a2810, #1a0f04);
+ border: 1px solid var(--guild-border);
+ border-radius: 5px;
+ color: var(--guild-text);
+ font-family: "Cinzel", serif;
+ font-size: 11px;
+ padding: 3px 10px;
+ cursor: pointer;
+ transition: .15s;
+}
+.gh-page-btn:hover:not(:disabled) { border-color: var(--guild-gold); }
+.gh-page-btn:disabled { opacity: .35; cursor: not-allowed; }
+.gh-page-btn.active {
+ background: linear-gradient(#6b4b2a, #3c2414);
+ border-color: var(--guild-gold);
+}
+
+/* ── Lade/Leer-Zustand ──────────────────────────────────── */
+.gh-loading, .gh-empty {
+ text-align: center;
+ padding: 40px;
+ font-family: "Cinzel", serif;
+ font-size: 13px;
+ color: var(--guild-muted);
+}
+
+/* ── Section-Header innerhalb Panel ────────────────────── */
+.gh-section-title {
+ font-family: "Cinzel", serif;
+ font-size: 12px;
+ color: var(--guild-gold);
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ padding: 8px 16px 4px;
+ border-bottom: 1px solid rgba(139,106,42,.25);
+ flex-shrink: 0;
+}
+
+/* ── Rang-Select in Mitglieder-Tabelle ──────────────────── */
+.gh-rank-select {
+ background: rgba(0,0,0,.5);
+ border: 1px solid var(--guild-border);
+ border-radius: 5px;
+ color: var(--guild-text);
+ font-family: "Cinzel", serif;
+ font-size: 11px;
+ padding: 2px 5px;
+ cursor: pointer;
+}
+.gh-kick-btn {
+ padding: 2px 8px;
+ background: linear-gradient(#4a1010, #2a0808);
+ border: 1px solid #8a3030;
+ border-radius: 5px;
+ color: #e07070;
+ font-family: "Cinzel", serif;
+ font-size: 10px;
+ cursor: pointer;
+ transition: .15s;
+}
+.gh-kick-btn:hover { border-color: #cc4040; }
diff --git a/public/js/buildings/gildenhalle.js b/public/js/buildings/gildenhalle.js
new file mode 100644
index 0000000..2505b60
--- /dev/null
+++ b/public/js/buildings/gildenhalle.js
@@ -0,0 +1,547 @@
+/* ============================================================
+ public/js/buildings/gildenhalle.js
+============================================================ */
+
+let gh_initialized = false;
+let gh_page = 1;
+let gh_search = '';
+let gh_playerGuild = null;
+
+function ghLoadCSS() {
+ if (!document.querySelector('link[href="/css/gildenhalle.css"]')) {
+ const l = document.createElement('link');
+ l.rel = 'stylesheet'; l.href = '/css/gildenhalle.css';
+ document.head.appendChild(l);
+ }
+}
+
+/* ── Popup sicherstellen ────────────────────────────────── */
+function ghEnsurePopup() {
+ if (document.getElementById('gildenhalle-popup')) return;
+
+ const popup = document.createElement('div');
+ popup.id = 'gildenhalle-popup';
+ popup.className = 'qm-popup';
+ popup.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
+
+ document.body.appendChild(popup);
+
+ popup.querySelector('#gh-close').addEventListener('click', ghClose);
+
+ /* Tab-Wechsel */
+ popup.querySelectorAll('.mp-tab').forEach(btn => {
+ btn.addEventListener('click', () => {
+ popup.querySelectorAll('.mp-tab').forEach(t => t.classList.remove('mp-tab-active'));
+ popup.querySelectorAll('.mp-panel').forEach(p => p.classList.remove('active'));
+ btn.classList.add('mp-tab-active');
+ const panel = document.getElementById(btn.dataset.tab);
+ panel?.classList.add('active');
+
+ if (btn.dataset.tab === 'gh-panel-2') ghLoadMyGuild();
+ if (btn.dataset.tab === 'gh-panel-3') ghLoadTasks();
+ });
+ });
+
+ /* Gründen-Toggle */
+ document.getElementById('gh-create-toggle-btn').addEventListener('click', () => {
+ document.getElementById('gh-create-form-wrap').style.display = 'block';
+ document.getElementById('gh-create-error').textContent = '';
+ });
+ document.getElementById('gh-create-cancel').addEventListener('click', () => {
+ document.getElementById('gh-create-form-wrap').style.display = 'none';
+ });
+ document.getElementById('gh-create-submit').addEventListener('click', ghCreateGuild);
+
+ /* Suche */
+ document.getElementById('gh-search-btn').addEventListener('click', () => {
+ gh_search = document.getElementById('gh-search-input').value.trim();
+ gh_page = 1;
+ ghLoadGuildList();
+ });
+ document.getElementById('gh-search-input').addEventListener('keydown', e => {
+ if (e.key === 'Enter') document.getElementById('gh-search-btn').click();
+ });
+
+ /* Drag */
+ const header = popup.querySelector('.qm-popup-header');
+ let dragging=false, sx,sy,sl,st;
+ header.style.cursor='grab';
+ header.addEventListener('mousedown', e => {
+ if (e.target.classList.contains('qm-popup-close')) return;
+ dragging=true; header.style.cursor='grabbing';
+ const r=popup.getBoundingClientRect();
+ sx=e.clientX; sy=e.clientY; sl=r.left; st=r.top;
+ popup.style.transform='none';
+ popup.style.left=sl+'px'; popup.style.top=st+'px';
+ e.preventDefault();
+ });
+ document.addEventListener('mousemove', e => {
+ if (!dragging) return;
+ popup.style.left=(sl+(e.clientX-sx))+'px';
+ popup.style.top=(st+(e.clientY-sy))+'px';
+ });
+ document.addEventListener('mouseup', () => { dragging=false; header.style.cursor='grab'; });
+}
+
+/* ════════════════════════════════════════════
+ PANEL 1: Gilden-Liste laden
+════════════════════════════════════════════ */
+async function ghLoadGuildList() {
+ const grid = document.getElementById('gh-guild-grid');
+ const pagination = document.getElementById('gh-pagination');
+ if (!grid) return;
+ grid.innerHTML = 'Lade Gilden…
';
+ if (pagination) pagination.innerHTML = '';
+
+ try {
+ const res = await fetch(`/api/gildenhalle/list?search=${encodeURIComponent(gh_search)}&page=${gh_page}`);
+ const data = await res.json();
+ gh_playerGuild = data.playerGuildId;
+
+ if (!data.guilds.length) {
+ grid.innerHTML = 'Keine Gilden gefunden.
';
+ return;
+ }
+
+ grid.innerHTML = data.guilds.map(g => `
+
+
+
${g.description || 'Keine Beschreibung'}
+
+ Lv. ${g.level} · ${g.member_count}/${g.max_members} Mitgl.
+
+ ${g.open ? '✔ Offen' : '🔒 Anfrage'}
+
+
+
+
`).join('');
+
+ grid.querySelectorAll('.gh-join-btn:not([disabled])').forEach(btn => {
+ btn.addEventListener('click', () => ghJoinGuild(
+ parseInt(btn.dataset.id), btn.dataset.open === '1', btn
+ ));
+ });
+
+ ghRenderPagination(pagination, data.totalPages, data.total);
+ } catch (err) {
+ grid.innerHTML = 'Fehler beim Laden.
';
+ console.error('[GH]', err);
+ }
+}
+
+/* ── Gilde beitreten ────────────────────────────────────── */
+async function ghJoinGuild(guildId, isOpen, btn) {
+ btn.disabled = true;
+ btn.textContent = '…';
+
+ let message = null;
+ if (!isOpen) {
+ message = window.prompt('Nachricht an die Gilde (optional):') ?? '';
+ }
+
+ try {
+ const res = await fetch(`/api/gildenhalle/join/${guildId}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ message }),
+ });
+ const data = await res.json();
+ if (!res.ok) { alert(data.error || 'Fehler'); btn.disabled=false; btn.textContent=isOpen?'⚔ Beitreten':'✉ Anfrage stellen'; return; }
+
+ if (data.joined) {
+ ghShowToast('✔ Erfolgreich der Gilde beigetreten!');
+ gh_page = 1; ghLoadGuildList();
+ } else {
+ btn.classList.add('pending');
+ btn.textContent = '⏳ Anfrage ausstehend';
+ ghShowToast('✉ Beitrittsanfrage gesendet!');
+ }
+ } catch { btn.disabled=false; }
+}
+
+/* ── Gilde gründen ──────────────────────────────────────── */
+async function ghCreateGuild() {
+ const name = document.getElementById('gh-new-name').value.trim();
+ const tag = document.getElementById('gh-new-tag').value.trim().toUpperCase();
+ const desc = document.getElementById('gh-new-desc').value.trim();
+ const open = document.getElementById('gh-new-open').value;
+ const errEl = document.getElementById('gh-create-error');
+
+ errEl.textContent = '';
+ if (!name || !tag) { errEl.textContent = 'Name und Tag sind Pflichtfelder.'; return; }
+
+ const btn = document.getElementById('gh-create-submit');
+ btn.disabled = true; btn.textContent = '…';
+
+ try {
+ const res = await fetch('/api/gildenhalle/create', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name, tag, description: desc, open: open === '1' }),
+ });
+ const data = await res.json();
+ if (!res.ok) { errEl.textContent = data.error || 'Fehler'; btn.disabled=false; btn.textContent='⚔ GILDE GRÜNDEN'; return; }
+
+ document.getElementById('gh-create-form-wrap').style.display = 'none';
+ ghShowToast('🏰 Gilde erfolgreich gegründet!');
+ gh_page = 1; ghLoadGuildList();
+
+ // Wechsel zu "Eigene Gilde"
+ setTimeout(() => {
+ document.querySelector('[data-tab="gh-panel-2"]')?.click();
+ }, 500);
+ } catch { btn.disabled=false; btn.textContent='⚔ GILDE GRÜNDEN'; }
+}
+
+/* ════════════════════════════════════════════
+ PANEL 2: Eigene Gilde
+════════════════════════════════════════════ */
+async function ghLoadMyGuild() {
+ const container = document.getElementById('gh-my-content');
+ if (!container) return;
+ container.innerHTML = 'Lade…
';
+
+ try {
+ const res = await fetch('/api/gildenhalle/my');
+ const data = await res.json();
+
+ if (!data.guild) {
+ container.innerHTML = `
+
+
🏰
+
Du bist noch in keiner Gilde.
+ Tritt einer Gilde bei oder gründe eine eigene.
+
`;
+ return;
+ }
+
+ const { guild, members, ranks, requests } = data;
+ const isLeader = (guild.leader_id === /* wird per Session gesetzt */ null);
+
+ // Mitglieder-Tabelle
+ const membersHtml = members.map(m => `
+
+ | ${m.name} |
+ Lv. ${m.level} |
+
+ ${guild.can_manage_ranks && m.id !== guild.leader_id
+ ? ``
+ : `${m.rank_name || '–'}`
+ }
+ |
+
+ ${new Date(m.joined_at).toLocaleDateString('de-DE')}
+ |
+
+ ${guild.can_kick && m.id !== guild.leader_id
+ ? ``
+ : ''}
+ |
+
`).join('');
+
+ // Anfragen
+ const requestsHtml = requests.length > 0
+ ? `Beitrittsanfragen (${requests.length})
+
+ ${requests.map(r => `
+
+ ${r.name}
+ ${r.message || '–'}
+ ${guild.can_invite
+ ? `
+ `
+ : ''}
+
`).join('')}
+
`
+ : '';
+
+ container.innerHTML = `
+
+
[${guild.tag}]
+
+
${guild.name}
+
Lv. ${guild.level} · ${members.length}/${guild.max_members} Mitglieder
+
+
+
+ ${requestsHtml}
+ Mitglieder
+
+
+
+
+ | Name | Level | Rang | Beigetreten | |
+
+
+ ${membersHtml}
+
+
`;
+ } catch (err) {
+ container.innerHTML = 'Fehler beim Laden.
';
+ console.error('[GH/my]', err);
+ }
+}
+
+/* ── Rang ändern ────────────────────────────────────────── */
+window.ghChangeRank = async function(userId, rankId) {
+ try {
+ const res = await fetch(`/api/gildenhalle/member/${userId}/rank`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ rank_id: rankId }),
+ });
+ if (!res.ok) { const d=await res.json(); alert(d.error||'Fehler'); ghLoadMyGuild(); }
+ else ghShowToast('✔ Rang geändert');
+ } catch {}
+};
+
+/* ── Mitglied kicken ────────────────────────────────────── */
+window.ghKickMember = async function(userId, name) {
+ if (!confirm(`${name} aus der Gilde entfernen?`)) return;
+ try {
+ const res = await fetch(`/api/gildenhalle/member/${userId}/kick`, { method: 'POST' });
+ const d = await res.json();
+ if (!res.ok) { alert(d.error||'Fehler'); return; }
+ ghShowToast(`✔ ${name} wurde entfernt`);
+ ghLoadMyGuild();
+ } catch {}
+};
+
+/* ── Anfrage annehmen/ablehnen ──────────────────────────── */
+window.ghHandleRequest = async function(requestId, action) {
+ try {
+ const res = await fetch(`/api/gildenhalle/requests/${requestId}/${action}`, { method: 'POST' });
+ const d = await res.json();
+ if (!res.ok) { alert(d.error||'Fehler'); return; }
+ ghShowToast(action === 'accept' ? '✔ Anfrage angenommen' : '✕ Anfrage abgelehnt');
+ ghLoadMyGuild();
+ } catch {}
+};
+
+/* ── Gilde verlassen ────────────────────────────────────── */
+window.ghLeaveGuild = async function() {
+ if (!confirm('Wirklich die Gilde verlassen?')) return;
+ try {
+ const res = await fetch('/api/gildenhalle/leave', { method: 'POST' });
+ const d = await res.json();
+ if (!res.ok) { alert(d.error||'Fehler'); return; }
+ ghShowToast('Gilde verlassen');
+ ghLoadMyGuild();
+ gh_page = 1; ghLoadGuildList();
+ } catch {}
+};
+
+/* ════════════════════════════════════════════
+ PANEL 3: Aufgaben
+════════════════════════════════════════════ */
+async function ghLoadTasks() {
+ const container = document.getElementById('gh-tasks-content');
+ if (!container) return;
+ container.innerHTML = 'Lade Aufgaben…
';
+
+ try {
+ const res = await fetch('/api/gildenhalle/tasks');
+ if (res.status === 400) {
+ container.innerHTML = `
+
+
📋
+
Tritt einer Gilde bei um gemeinsame Aufgaben zu sehen.
+
`;
+ return;
+ }
+ const data = await res.json();
+
+ if (!data.tasks?.length) {
+ container.innerHTML = 'Keine Aufgaben für heute.
';
+ return;
+ }
+
+ const rewardIcons = { gold: '🪙', gems: '💎', wood: '🪵', iron: '⚙️' };
+
+ container.innerHTML = `
+
+ Gemeinsame Aufgaben der Gilde ${data.guildName}
+
+
+ ${data.tasks.map(t => {
+ const pct = Math.min(100, Math.round((t.current_amount / t.target_amount) * 100));
+ const done = t.completed || t.current_amount >= t.target_amount;
+ const icon = rewardIcons[t.reward_type] || '🪙';
+ return `
+
+
+
+
+
+ ${t.current_amount} / ${t.target_amount}
+ ${pct}% · Mein Beitrag: ${t.my_contribution || 0}
+
+
+
`;
+ }).join('')}
+
`;
+ } catch (err) {
+ container.innerHTML = 'Fehler beim Laden.
';
+ console.error('[GH/tasks]', err);
+ }
+}
+
+/* ── Pagination ─────────────────────────────────────────── */
+function ghRenderPagination(el, totalPages, total) {
+ if (!el || !totalPages || totalPages <= 1) return;
+ el.innerHTML = `
+
+ ${Array.from({length:totalPages},(_,i)=>i+1).map(p =>
+ ``
+ ).join('')}
+ `;
+ el.querySelector('#gh-prev')?.addEventListener('click', () => { if(gh_page>1){gh_page--;ghLoadGuildList();} });
+ el.querySelector('#gh-next')?.addEventListener('click', () => { if(gh_page
+ btn.addEventListener('click', () => { gh_page=parseInt(btn.dataset.page); ghLoadGuildList(); })
+ );
+}
+
+/* ── Toast ──────────────────────────────────────────────── */
+function ghShowToast(msg) {
+ const t = document.createElement('div');
+ t.className = 'baz-toast'; t.textContent = msg;
+ document.body.appendChild(t);
+ setTimeout(() => t.remove(), 2800);
+}
+
+/* ── Schließen ──────────────────────────────────────────── */
+function ghClose() {
+ document.getElementById('gildenhalle-popup')?.classList.remove('active');
+ document.getElementById('qm-overlay')?.classList.remove('active');
+}
+
+/* ════════════════════════════════════════════
+ EXPORT
+════════════════════════════════════════════ */
+export function loadGildenhalle() {
+ ghLoadCSS();
+ ghEnsurePopup();
+
+ const popup = document.getElementById('gildenhalle-popup');
+ const overlay = document.getElementById('qm-overlay');
+ popup.style.left = '50%'; popup.style.top = '50%';
+ popup.style.transform = 'translate(-50%,-50%) scale(1)';
+ popup.classList.add('active');
+ overlay?.classList.add('active');
+
+ if (!gh_initialized) {
+ gh_initialized = true;
+ ghLoadGuildList();
+ }
+}
diff --git a/public/js/map-ui.js b/public/js/map-ui.js
index 3fb4833..0e4c046 100644
--- a/public/js/map-ui.js
+++ b/public/js/map-ui.js
@@ -5,6 +5,8 @@ import { loadBlackmarket } from "./buildings/blackmarket.js?v=2";
import { loadMine } from "./buildings/mine.js?v=2";
import { loadGlucksspiel } from "./gaststaette/glucksspiel.js";
import { loadBazaar } from "./buildings/bazaar.js";
+import { loadGildenhalle } from "./buildings/gildenhalle.js";
+
const popup = document.getElementById("building-popup");
const title = document.getElementById("popup-title");
const tooltip = document.getElementById("map-tooltip");
@@ -16,12 +18,16 @@ const tooltipCache = {};
================================ */
const gaststaettePopup = document.getElementById("gaststaette-popup");
-const qmOverlay = document.getElementById("qm-overlay");
+const qmOverlay = document.getElementById("qm-overlay");
document.querySelectorAll(".gaststaette-tab").forEach((tab) => {
tab.addEventListener("click", () => {
- document.querySelectorAll(".gaststaette-tab").forEach((t) => t.classList.remove("active"));
- document.querySelectorAll(".gaststaette-tab-content").forEach((c) => c.classList.remove("active"));
+ document
+ .querySelectorAll(".gaststaette-tab")
+ .forEach((t) => t.classList.remove("active"));
+ document
+ .querySelectorAll(".gaststaette-tab-content")
+ .forEach((c) => c.classList.remove("active"));
tab.classList.add("active");
document.getElementById(tab.dataset.tab).classList.add("active");
if (tab.dataset.tab === "gs-tab1") loadGlucksspiel();
@@ -49,7 +55,7 @@ document.getElementById("qm-overlay")?.addEventListener("mouseenter", () => {
});
const buildingModules = {
- 1: loadArena, // Arena – eigenes UI, Tabs ausblenden
+ 1: loadArena, // Arena – eigenes UI, Tabs ausblenden
3: loadHimmelstor, // Himmelstor – eigenes UI, Tabs ausblenden
10: loadMine, // Mine – Tabs bleiben sichtbar
11: loadCharacterHouse, // Charakterhaus – eigenes UI, Tabs ausblenden
@@ -182,7 +188,7 @@ document.querySelectorAll(".building").forEach((building) => {
// Gebäude 6 → Gaststätte eigenes Popup
if (url === "/building/6") {
gaststaettePopup.style.left = "50%";
- gaststaettePopup.style.top = "50%";
+ gaststaettePopup.style.top = "50%";
gaststaettePopup.style.transform = "translate(-50%, -50%) scale(1)";
gaststaettePopup.classList.add("active");
qmOverlay.classList.add("active");
@@ -196,6 +202,12 @@ document.querySelectorAll(".building").forEach((building) => {
return;
}
+ // Gebäude 9 → Gildenhalle eigenes Popup
+ if (url === "/building/9") {
+ loadGildenhalle();
+ return;
+ }
+
await openBuildingPopup(url);
});
});
@@ -287,7 +299,8 @@ document.querySelectorAll(".building").forEach((building) => {
building.addEventListener("mouseenter", async (e) => {
try {
// data-id bevorzugen, sonst ID aus href extrahieren (z.B. "/building/5" → "5")
- const id = building.dataset.id || building.getAttribute("href")?.split("/").pop();
+ const id =
+ building.dataset.id || building.getAttribute("href")?.split("/").pop();
if (!id) return;
if (!tooltipCache[id]) {
diff --git a/routes/gildenhalle.route.js b/routes/gildenhalle.route.js
new file mode 100644
index 0000000..f69fe9e
--- /dev/null
+++ b/routes/gildenhalle.route.js
@@ -0,0 +1,414 @@
+/* ============================================================
+ routes/gildenhalle.route.js
+============================================================ */
+'use strict';
+
+const express = require('express');
+const router = express.Router();
+const db = require('../database/database');
+
+function requireLogin(req, res, next) {
+ if (!req.session?.user) return res.status(401).json({ error: 'Nicht eingeloggt' });
+ next();
+}
+
+/* ── Hilfsfunktion: Spieler-Gilde laden ────────────────── */
+async function getPlayerGuild(userId) {
+ const [[row]] = await db.query(
+ `SELECT g.*, gm.rank_id, gr.name AS rank_name, gr.sort_order,
+ gr.can_invite, gr.can_kick, gr.can_manage_ranks, gr.can_manage_tasks
+ FROM guild_members gm
+ JOIN guilds g ON g.id = gm.guild_id
+ LEFT JOIN guild_ranks gr ON gr.id = gm.rank_id
+ WHERE gm.user_id = ?`,
+ [userId]
+ );
+ return row || null;
+}
+
+/* ── Standard-Ränge für neue Gilde anlegen ─────────────── */
+async function createDefaultRanks(guildId) {
+ const ranks = [
+ { name: 'Gildenmeister', sort_order: 1, can_invite: 1, can_kick: 1, can_manage_ranks: 1, can_manage_tasks: 1 },
+ { name: 'Offizier', sort_order: 2, can_invite: 1, can_kick: 1, can_manage_ranks: 0, can_manage_tasks: 1 },
+ { name: 'Veteran', sort_order: 3, can_invite: 1, can_kick: 0, can_manage_ranks: 0, can_manage_tasks: 0 },
+ { name: 'Mitglied', sort_order: 99, can_invite: 0, can_kick: 0, can_manage_ranks: 0, can_manage_tasks: 0 },
+ ];
+ const ids = [];
+ for (const r of ranks) {
+ const [res] = await db.query(
+ `INSERT INTO guild_ranks (guild_id, name, sort_order, can_invite, can_kick, can_manage_ranks, can_manage_tasks)
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
+ [guildId, r.name, r.sort_order, r.can_invite, r.can_kick, r.can_manage_ranks, r.can_manage_tasks]
+ );
+ ids.push({ name: r.name, id: res.insertId });
+ }
+ return ids;
+}
+
+/* ── Tagesaufgaben für Gilde generieren ────────────────── */
+async function ensureDailyTasks(guildId) {
+ const today = new Date().toISOString().slice(0, 10);
+ const [existing] = await db.query(
+ 'SELECT id FROM guild_tasks WHERE guild_id = ? AND expires_at = ?',
+ [guildId, today]
+ );
+ if (existing.length > 0) return;
+
+ const [templates] = await db.query(
+ 'SELECT * FROM guild_task_templates WHERE active = 1'
+ );
+ for (const t of templates) {
+ await db.query(
+ `INSERT IGNORE INTO guild_tasks
+ (guild_id, task_key, label, target_amount, reward_type, reward_amount, expires_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
+ [guildId, t.task_key, t.label, t.target_amount, t.reward_type, t.reward_amount, today]
+ );
+ }
+}
+
+/* ════════════════════════════════════════════
+ GET /api/gildenhalle/list?search=&page=1
+ Alle Gilden auflisten
+════════════════════════════════════════════ */
+router.get('/gildenhalle/list', requireLogin, async (req, res) => {
+ const search = req.query.search?.trim() || '';
+ const page = Math.max(1, parseInt(req.query.page) || 1);
+ const limit = 12;
+ const offset = (page - 1) * limit;
+ const userId = req.session.user.id;
+
+ try {
+ const searchParam = search ? `%${search}%` : '%';
+ const [guilds] = await db.query(
+ `SELECT g.id, g.name, g.tag, g.description, g.level, g.open, g.max_members,
+ COUNT(gm.id) AS member_count,
+ a.ingame_name AS leader_name
+ FROM guilds g
+ LEFT JOIN guild_members gm ON gm.guild_id = g.id
+ LEFT JOIN accounts a ON a.id = g.leader_id
+ WHERE g.name LIKE ? OR g.tag LIKE ?
+ GROUP BY g.id
+ ORDER BY g.level DESC, member_count DESC
+ LIMIT ? OFFSET ?`,
+ [searchParam, searchParam, limit, offset]
+ );
+
+ const [[{ total }]] = await db.query(
+ 'SELECT COUNT(*) AS total FROM guilds WHERE name LIKE ? OR tag LIKE ?',
+ [searchParam, searchParam]
+ );
+
+ // Offene Anfrage des Spielers prüfen
+ const [myRequests] = await db.query(
+ "SELECT guild_id FROM guild_join_requests WHERE user_id = ? AND status = 'pending'",
+ [userId]
+ );
+ const pendingIds = new Set(myRequests.map(r => r.guild_id));
+
+ const playerGuild = await getPlayerGuild(userId);
+
+ res.json({
+ guilds: guilds.map(g => ({
+ ...g,
+ has_pending_request: pendingIds.has(g.id),
+ })),
+ total,
+ totalPages: Math.ceil(total / limit),
+ page,
+ playerGuildId: playerGuild?.id || null,
+ });
+ } catch (err) {
+ console.error('[gildenhalle/list]', err);
+ res.status(500).json({ error: 'DB Fehler' });
+ }
+});
+
+/* ════════════════════════════════════════════
+ POST /api/gildenhalle/create
+ Neue Gilde gründen
+════════════════════════════════════════════ */
+router.post('/gildenhalle/create', requireLogin, async (req, res) => {
+ const userId = req.session.user.id;
+ const { name, tag, description, open } = req.body;
+
+ if (!name?.trim() || !tag?.trim())
+ return res.status(400).json({ error: 'Name und Tag sind Pflichtfelder.' });
+ if (tag.length < 2 || tag.length > 6)
+ return res.status(400).json({ error: 'Tag muss 2–6 Zeichen lang sein.' });
+
+ try {
+ const existing = await getPlayerGuild(userId);
+ if (existing) return res.status(400).json({ error: 'Du bist bereits in einer Gilde.' });
+
+ const [result] = await db.query(
+ `INSERT INTO guilds (name, tag, description, leader_id, open)
+ VALUES (?, ?, ?, ?, ?)`,
+ [name.trim(), tag.trim().toUpperCase(), description?.trim() || null, userId, open ? 1 : 0]
+ );
+ const guildId = result.insertId;
+
+ // Standard-Ränge anlegen
+ const rankIds = await createDefaultRanks(guildId);
+ const leaderRankId = rankIds.find(r => r.name === 'Gildenmeister')?.id;
+
+ // Gründer als Mitglied mit höchstem Rang eintragen
+ await db.query(
+ 'INSERT INTO guild_members (guild_id, user_id, rank_id) VALUES (?, ?, ?)',
+ [guildId, userId, leaderRankId]
+ );
+
+ // Erste Tagesaufgaben generieren
+ await ensureDailyTasks(guildId);
+
+ res.json({ success: true, guildId });
+ } catch (err) {
+ if (err.code === 'ER_DUP_ENTRY')
+ return res.status(400).json({ error: 'Gildenname oder Tag bereits vergeben.' });
+ console.error('[gildenhalle/create]', err);
+ res.status(500).json({ error: 'DB Fehler' });
+ }
+});
+
+/* ════════════════════════════════════════════
+ POST /api/gildenhalle/join/:id
+ Gilde beitreten oder Anfrage stellen
+════════════════════════════════════════════ */
+router.post('/gildenhalle/join/:id', requireLogin, async (req, res) => {
+ const userId = req.session.user.id;
+ const guildId = parseInt(req.params.id);
+
+ try {
+ const existing = await getPlayerGuild(userId);
+ if (existing) return res.status(400).json({ error: 'Du bist bereits in einer Gilde.' });
+
+ const [[guild]] = await db.query('SELECT * FROM guilds WHERE id = ?', [guildId]);
+ if (!guild) return res.status(404).json({ error: 'Gilde nicht gefunden.' });
+
+ const [[memberCount]] = await db.query(
+ 'SELECT COUNT(*) AS c FROM guild_members WHERE guild_id = ?', [guildId]
+ );
+ if (memberCount.c >= guild.max_members)
+ return res.status(400).json({ error: 'Gilde ist voll.' });
+
+ if (guild.open) {
+ // Standard-Rang (Mitglied = höchste sort_order)
+ const [[defaultRank]] = await db.query(
+ 'SELECT id FROM guild_ranks WHERE guild_id = ? ORDER BY sort_order DESC LIMIT 1',
+ [guildId]
+ );
+ await db.query(
+ 'INSERT INTO guild_members (guild_id, user_id, rank_id) VALUES (?, ?, ?)',
+ [guildId, userId, defaultRank?.id || null]
+ );
+ res.json({ success: true, joined: true });
+ } else {
+ // Anfrage stellen
+ await db.query(
+ `INSERT IGNORE INTO guild_join_requests (guild_id, user_id, message)
+ VALUES (?, ?, ?)`,
+ [guildId, userId, req.body.message?.trim() || null]
+ );
+ res.json({ success: true, joined: false, requested: true });
+ }
+ } catch (err) {
+ if (err.code === 'ER_DUP_ENTRY')
+ return res.status(400).json({ error: 'Anfrage bereits gestellt.' });
+ console.error('[gildenhalle/join]', err);
+ res.status(500).json({ error: 'DB Fehler' });
+ }
+});
+
+/* ════════════════════════════════════════════
+ POST /api/gildenhalle/leave
+ Gilde verlassen
+════════════════════════════════════════════ */
+router.post('/gildenhalle/leave', requireLogin, async (req, res) => {
+ const userId = req.session.user.id;
+ try {
+ const guild = await getPlayerGuild(userId);
+ if (!guild) return res.status(400).json({ error: 'Du bist in keiner Gilde.' });
+ if (guild.leader_id === userId)
+ return res.status(400).json({ error: 'Als Gildenleiterin kannst du nicht austreten. Übertrage zuerst die Leitung.' });
+
+ await db.query('DELETE FROM guild_members WHERE user_id = ? AND guild_id = ?', [userId, guild.id]);
+ res.json({ success: true });
+ } catch (err) {
+ console.error('[gildenhalle/leave]', err);
+ res.status(500).json({ error: 'DB Fehler' });
+ }
+});
+
+/* ════════════════════════════════════════════
+ GET /api/gildenhalle/my
+ Eigene Gilde + Mitglieder laden
+════════════════════════════════════════════ */
+router.get('/gildenhalle/my', requireLogin, async (req, res) => {
+ const userId = req.session.user.id;
+ try {
+ const guild = await getPlayerGuild(userId);
+ if (!guild) return res.json({ guild: null });
+
+ await ensureDailyTasks(guild.id);
+
+ const [members] = await db.query(
+ `SELECT a.id, a.ingame_name AS name, a.level,
+ gr.id AS rank_id, gr.name AS rank_name, gr.sort_order,
+ gm.joined_at
+ FROM guild_members gm
+ JOIN accounts a ON a.id = gm.user_id
+ LEFT JOIN guild_ranks gr ON gr.id = gm.rank_id
+ WHERE gm.guild_id = ?
+ ORDER BY gr.sort_order ASC, gm.joined_at ASC`,
+ [guild.id]
+ );
+
+ const [ranks] = await db.query(
+ 'SELECT * FROM guild_ranks WHERE guild_id = ? ORDER BY sort_order ASC',
+ [guild.id]
+ );
+
+ const [requests] = await db.query(
+ `SELECT gjr.id, gjr.user_id, a.ingame_name AS name, gjr.message, gjr.created_at
+ FROM guild_join_requests gjr
+ JOIN accounts a ON a.id = gjr.user_id
+ WHERE gjr.guild_id = ? AND gjr.status = 'pending'`,
+ [guild.id]
+ );
+
+ res.json({ guild, members, ranks, requests });
+ } catch (err) {
+ console.error('[gildenhalle/my]', err);
+ res.status(500).json({ error: 'DB Fehler' });
+ }
+});
+
+/* ════════════════════════════════════════════
+ GET /api/gildenhalle/tasks
+ Gilden-Tagesaufgaben laden
+════════════════════════════════════════════ */
+router.get('/gildenhalle/tasks', requireLogin, async (req, res) => {
+ const userId = req.session.user.id;
+ try {
+ const guild = await getPlayerGuild(userId);
+ if (!guild) return res.status(400).json({ error: 'Du bist in keiner Gilde.' });
+
+ await ensureDailyTasks(guild.id);
+ const today = new Date().toISOString().slice(0, 10);
+
+ const [tasks] = await db.query(
+ `SELECT t.*,
+ COALESCE(SUM(c.amount), 0) AS contributed_total,
+ (SELECT COALESCE(SUM(c2.amount),0) FROM guild_task_contributions c2
+ WHERE c2.task_id = t.id AND c2.user_id = ?) AS my_contribution
+ FROM guild_tasks t
+ LEFT JOIN guild_task_contributions c ON c.task_id = t.id
+ WHERE t.guild_id = ? AND t.expires_at = ?
+ GROUP BY t.id`,
+ [userId, guild.id, today]
+ );
+
+ res.json({ tasks, guildName: guild.name });
+ } catch (err) {
+ console.error('[gildenhalle/tasks]', err);
+ res.status(500).json({ error: 'DB Fehler' });
+ }
+});
+
+/* ════════════════════════════════════════════
+ POST /api/gildenhalle/requests/:id/accept
+ POST /api/gildenhalle/requests/:id/reject
+ Beitrittsanfragen verwalten
+════════════════════════════════════════════ */
+router.post('/gildenhalle/requests/:id/:action', requireLogin, async (req, res) => {
+ const userId = req.session.user.id;
+ const requestId = parseInt(req.params.id);
+ const action = req.params.action; // accept | reject
+
+ if (!['accept', 'reject'].includes(action))
+ return res.status(400).json({ error: 'Ungültige Aktion.' });
+
+ try {
+ const guild = await getPlayerGuild(userId);
+ if (!guild?.can_invite) return res.status(403).json({ error: 'Keine Berechtigung.' });
+
+ const [[request]] = await db.query(
+ "SELECT * FROM guild_join_requests WHERE id = ? AND guild_id = ? AND status = 'pending'",
+ [requestId, guild.id]
+ );
+ if (!request) return res.status(404).json({ error: 'Anfrage nicht gefunden.' });
+
+ await db.query(
+ 'UPDATE guild_join_requests SET status = ? WHERE id = ?',
+ [action === 'accept' ? 'accepted' : 'rejected', requestId]
+ );
+
+ if (action === 'accept') {
+ const [[defaultRank]] = await db.query(
+ 'SELECT id FROM guild_ranks WHERE guild_id = ? ORDER BY sort_order DESC LIMIT 1',
+ [guild.id]
+ );
+ await db.query(
+ 'INSERT IGNORE INTO guild_members (guild_id, user_id, rank_id) VALUES (?, ?, ?)',
+ [guild.id, request.user_id, defaultRank?.id || null]
+ );
+ }
+
+ res.json({ success: true });
+ } catch (err) {
+ console.error('[gildenhalle/requests]', err);
+ res.status(500).json({ error: 'DB Fehler' });
+ }
+});
+
+/* ════════════════════════════════════════════
+ POST /api/gildenhalle/member/:userId/rank
+ Rang eines Mitglieds ändern
+════════════════════════════════════════════ */
+router.post('/gildenhalle/member/:targetId/rank', requireLogin, async (req, res) => {
+ const userId = req.session.user.id;
+ const targetId = parseInt(req.params.targetId);
+ const { rank_id } = req.body;
+
+ try {
+ const guild = await getPlayerGuild(userId);
+ if (!guild?.can_manage_ranks) return res.status(403).json({ error: 'Keine Berechtigung.' });
+ if (targetId === guild.leader_id) return res.status(403).json({ error: 'Rang des Gildenmeisters kann nicht geändert werden.' });
+
+ await db.query(
+ 'UPDATE guild_members SET rank_id = ? WHERE user_id = ? AND guild_id = ?',
+ [rank_id, targetId, guild.id]
+ );
+ res.json({ success: true });
+ } catch (err) {
+ console.error('[gildenhalle/member/rank]', err);
+ res.status(500).json({ error: 'DB Fehler' });
+ }
+});
+
+/* ════════════════════════════════════════════
+ POST /api/gildenhalle/member/:userId/kick
+ Mitglied entfernen
+════════════════════════════════════════════ */
+router.post('/gildenhalle/member/:targetId/kick', requireLogin, async (req, res) => {
+ const userId = req.session.user.id;
+ const targetId = parseInt(req.params.targetId);
+
+ try {
+ const guild = await getPlayerGuild(userId);
+ if (!guild?.can_kick) return res.status(403).json({ error: 'Keine Berechtigung.' });
+ if (targetId === guild.leader_id) return res.status(403).json({ error: 'Gildenmeister kann nicht gekickt werden.' });
+
+ await db.query(
+ 'DELETE FROM guild_members WHERE user_id = ? AND guild_id = ?',
+ [targetId, guild.id]
+ );
+ res.json({ success: true });
+ } catch (err) {
+ console.error('[gildenhalle/member/kick]', err);
+ res.status(500).json({ error: 'DB Fehler' });
+ }
+});
+
+module.exports = router;
diff --git a/views/launcher.ejs b/views/launcher.ejs
index 91e0dad..89c6035 100644
--- a/views/launcher.ejs
+++ b/views/launcher.ejs
@@ -97,6 +97,7 @@
+