Files
Webklar-Kundenbereich/public/admin.html
T
ClaudeandClaude Opus 5.5 37489aa866 Admin-Chat: neue Unterhaltung mit jedem Kunden starten
Ueber der Chat-Liste sucht ein Feld alle Kunden (Name, E-Mail,
Kd.-Nr.) und oeffnet einen leeren Chat, auch wenn der Kunde noch
nie geschrieben hat. Der Server konnte das schon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:51:57 +00:00

310 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin – Webklar Kundenbereich</title>
<link rel="stylesheet" href="/login.css">
<style>
.admin-tabs { display: flex; gap: 8px; margin: 18px 0; flex-wrap: wrap; }
.admin-tab {
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.14);
color: inherit; padding: 8px 16px; border-radius: 999px; cursor: pointer; font: inherit;
}
.admin-tab.active { background: rgba(94,234,212,0.16); border-color: rgba(94,234,212,0.5); }
.admin-section { display: none; }
.admin-section.active { display: block; }
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 18px; }
.stat-tile { padding: 14px 16px; border-radius: 12px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); }
.stat-tile .num { font-size: 26px; font-weight: 700; display: block; }
.stat-tile .lbl { font-size: 12px; opacity: 0.7; }
.admin-search { width: 100%; max-width: 380px; padding: 9px 14px; border-radius: 10px;
border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.06); color: inherit; margin-bottom: 12px; }
.admin-table-wrap { overflow-x: auto; border-radius: 12px; border: 1px solid rgba(255,255,255,0.1); }
/* Kundentabelle: volle Breite, kein Links-/Rechts-Scroll */
.admin-table-wrap.no-scroll { overflow-x: visible; }
.admin-table-wrap.no-scroll table.admin-table { table-layout: fixed; }
.admin-table-wrap.no-scroll .admin-table td { word-break: break-word; overflow-wrap: anywhere; }
.cust-actions { display: flex; flex-wrap: wrap; gap: 6px; }
table.admin-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.admin-table th, .admin-table td { text-align: left; padding: 9px 12px; border-bottom: 1px solid rgba(255,255,255,0.08); vertical-align: middle; }
.admin-table th { font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.65; }
.admin-table tr:last-child td { border-bottom: none; }
.pill { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; border: 1px solid transparent; }
.pill-ok { background: rgba(74,222,128,0.15); border-color: rgba(74,222,128,0.4); }
.pill-warn { background: rgba(251,191,36,0.15); border-color: rgba(251,191,36,0.45); }
.pill-muted { background: rgba(255,255,255,0.07); border-color: rgba(255,255,255,0.16); opacity: 0.8; }
.pill-red { background: rgba(248,113,113,0.15); border-color: rgba(248,113,113,0.45); }
.mini-btn { background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.18); color: inherit;
padding: 5px 11px; border-radius: 8px; cursor: pointer; font-size: 13px; white-space: nowrap; }
.mini-btn:hover { background: rgba(255,255,255,0.14); }
a.mini-btn { display: inline-flex; align-items: center; text-decoration: none; }
.mini-btn.primary { background: rgba(94,234,212,0.16); border-color: rgba(94,234,212,0.5); }
.code-input { width: 100%; min-width: 60px; max-width: 90px; padding: 4px 8px; border-radius: 7px; border: 1px solid rgba(255,255,255,0.2);
background: rgba(255,255,255,0.06); color: inherit; font: inherit; font-size: 13px; }
.detail-row td { background: rgba(255,255,255,0.03); }
.detail-box { padding: 6px 4px; }
.detail-box h4 { margin: 8px 0 4px; font-size: 13px; }
.detail-box .muted { opacity: 0.65; }
.ws-line { font-size: 13px; opacity: 0.85; margin: 2px 0 2px 14px; }
a.plain { color: #7dd3fc; text-decoration: none; }
a.plain:hover { text-decoration: underline; }
.admin-note { font-size: 13px; opacity: 0.65; margin: 8px 0 0; }
.flex-between { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; }
.recent-list { list-style: none; margin: 0; padding: 0; }
.recent-list li { padding: 7px 0; border-bottom: 1px solid rgba(255,255,255,0.07); font-size: 14px; }
.recent-list li:last-child { border-bottom: none; }
.two-col { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 14px; }
/* Chat */
.badge-pill {
display: inline-flex; min-width: 20px; height: 20px; padding: 0 6px;
align-items: center; justify-content: center; border-radius: 999px;
background: #f87171; color: #fff; font-size: 12px; font-weight: 700;
}
.chat-layout { display: grid; grid-template-columns: 280px 1fr; gap: 14px; min-height: 480px; }
@media (max-width: 760px) { .chat-layout { grid-template-columns: 1fr; } }
.chat-side { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.chat-list { border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; overflow-y: auto; max-height: 560px; }
.chat-list-item { padding: 12px 14px; border-bottom: 1px solid rgba(255,255,255,0.07); cursor: pointer; }
.chat-list-item:hover { background: rgba(255,255,255,0.05); }
.chat-list-item.active { background: rgba(94,234,212,0.1); }
.chat-list-item .last { font-size: 12px; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-thread { display: flex; flex-direction: column; border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 12px; }
.chat-messages { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; padding: 4px; max-height: 440px; }
.chat-bubble { max-width: 78%; padding: 10px 14px; border-radius: 16px; font-size: 14px; white-space: pre-wrap; word-break: break-word; }
.chat-bubble .chat-meta { display: block; font-size: 11px; opacity: 0.6; margin-top: 4px; }
.chat-bubble.mine { align-self: flex-end; background: rgba(94,234,212,0.18); border: 1px solid rgba(94,234,212,0.45); border-bottom-right-radius: 4px; }
.chat-bubble.theirs { align-self: flex-start; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.14); border-bottom-left-radius: 4px; }
.chat-input-row { display: flex; gap: 10px; margin-top: 12px; align-items: flex-end; }
.chat-input-row textarea {
flex: 1; resize: none; min-height: 44px; max-height: 120px; padding: 10px 12px;
border-radius: 12px; border: 1px solid rgba(255,255,255,0.18);
background: rgba(255,255,255,0.06); color: inherit; font: inherit;
}
.chat-empty { display: flex; align-items: center; justify-content: center; flex: 1; opacity: 0.6; }
/* Anhaenge und Sprachnachrichten (chat-composer.css) im Tuerkis des Admin-Panels */
body {
--chat-accent: #2dd4bf; --chat-accent-hover: #5eead4; --chat-accent-soft: rgba(94,234,212,0.16);
--chat-accent-line: rgba(94,234,212,0.5); --chat-accent-text: #5eead4; --chat-on-accent: #042f2e;
}
/* Übernahmegutschriften */
.credit-filter { display: flex; gap: 8px; margin-bottom: 12px; }
.credit-filter button {
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.14);
color: inherit; padding: 5px 13px; border-radius: 999px; cursor: pointer; font: inherit; font-size: 13px;
}
.credit-filter button.active { background: rgba(94,234,212,0.16); border-color: rgba(94,234,212,0.5); }
tr.credit-active td { background: rgba(94,234,212,0.07); }
.credit-detail { margin-top: 18px; }
.credit-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
.credit-head h3 { margin: 0; }
.credit-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 22px; }
.credit-cols h4 { margin: 16px 0 6px; font-size: 13px; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; }
.credit-cols h4:first-child { margin-top: 4px; }
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 14px; font-size: 14px; margin: 0; }
.kv dt { opacity: 0.6; }
.kv dd { margin: 0; overflow-wrap: anywhere; }
.pre { white-space: pre-wrap; font-size: 14px; margin: 0; overflow-wrap: anywhere; }
.check-list { list-style: none; margin: 0; padding: 0; font-size: 14px; }
.check-list li { padding: 5px 0 5px 26px; position: relative; border-bottom: 1px solid rgba(255,255,255,0.06); }
.check-list li:last-child { border-bottom: none; }
.check-list li::before { position: absolute; left: 2px; top: 5px; font-weight: 700; }
.check-list li.ok::before { content: '✓'; color: #4ade80; }
.check-list li.warn::before { content: '!'; color: #fbbf24; left: 6px; }
.check-list li.info::before { content: 'i'; color: #7dd3fc; left: 6px; font-style: italic; }
.check-list small { display: block; opacity: 0.6; font-size: 12px; overflow-wrap: anywhere; }
.manual-checks label { display: flex; gap: 9px; align-items: flex-start; font-size: 14px; padding: 4px 0; cursor: pointer; }
.manual-checks input { margin-top: 3px; width: 16px; height: 16px; accent-color: #5eead4; flex: none; }
.calc-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; }
.calc-grid label, .credit-field label { display: block; font-size: 12px; opacity: 0.7; margin-bottom: 4px; }
.credit-input { width: 100%; box-sizing: border-box; padding: 7px 10px; border-radius: 9px; border: 1px solid rgba(255,255,255,0.2);
background: rgba(255,255,255,0.06); color: inherit; font: inherit; font-size: 14px; }
.credit-input option { color: #111; }
textarea.credit-input { min-height: 80px; resize: vertical; }
.calc-result { margin: 10px 0; padding: 9px 12px; border-radius: 10px; font-size: 14px; background: rgba(94,234,212,0.08); border: 1px solid rgba(94,234,212,0.3); }
/* E-Mail-Speicher: ab der Warnschwelle des Hetzner-Pakets rot */
.calc-result.warn { background: rgba(248,113,113,0.12); border-color: rgba(248,113,113,0.45); color: #fca5a5; }
.storage-task strong { font-size: 15px; }
.storage-partial { color: #fca5a5; }
.storage-failed { margin: 14px 0 0; }
.storage-failed li { margin: 4px 0; font-size: 14px; }
.credit-field { margin-top: 10px; }
.credit-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.mini-btn:disabled { opacity: 0.45; cursor: default; }
.mini-btn.danger { border-color: rgba(248,113,113,0.5); }
.credit-msg { margin-top: 10px; font-size: 14px; }
.history-item { font-size: 13px; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.06); }
.history-item:last-child { border-bottom: none; }
</style>
<link rel="stylesheet" href="/chat-composer.css">
</head>
<body class="portal-page dashboard-page">
<div class="dot-background" aria-hidden="true">
<div class="dot-pattern"></div>
<div class="dot-fade"></div>
</div>
<header class="portal-header">
<a class="portal-logo" href="/admin.html">Webklar <span style="opacity:.6">Admin</span></a>
<div class="portal-header-actions">
<span class="portal-meta" id="admin-meta"></span>
<a class="portal-btn-outline" href="https://ticket.webklar.com/tickets">← Ticketsystem</a>
<button type="button" class="portal-btn-outline" id="logout-btn">Abmelden</button>
</div>
</header>
<main class="portal-main dashboard-content-reveal">
<h1 class="portal-title">Admin-Übersicht</h1>
<p id="admin-error" class="error hidden"></p>
<div class="admin-tabs">
<button class="admin-tab active" data-tab="overview">Übersicht</button>
<button class="admin-tab" data-tab="projects">Projekte</button>
<button class="admin-tab" data-tab="customers">Kunden</button>
<button class="admin-tab" data-tab="chat">Chat <span id="admin-chat-badge" class="badge-pill hidden"></span></button>
<button class="admin-tab" data-tab="credits">Gutschriften <span id="admin-credit-badge" class="badge-pill hidden"></span></button>
<button class="admin-tab" data-tab="offers">Abo-Angebote</button>
<button class="admin-tab" data-tab="storage">E-Mail-Speicher</button>
</div>
<!-- Übersicht -->
<section class="admin-section active" id="tab-overview">
<div class="stat-grid" id="stat-grid"><span class="portal-status">Lade…</span></div>
<div class="two-col">
<div class="portal-card">
<h3>Letzte Portal-Logins</h3>
<ul class="recent-list" id="recent-logins"><li class="muted">Lade…</li></ul>
</div>
<div class="portal-card">
<h3>Zuletzt aktualisierte Projekte</h3>
<ul class="recent-list" id="recent-projects"><li class="muted">Lade…</li></ul>
</div>
</div>
</section>
<!-- Projekte -->
<section class="admin-section" id="tab-projects">
<input class="admin-search" id="project-search" type="search"
placeholder="Projekte suchen (Name, Subdomain, Repo, Kunde)…">
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<tr><th>Projekt</th><th>Subdomain</th><th>Kunde</th><th>Status</th><th>Typ</th><th>Links</th></tr>
</thead>
<tbody id="project-rows"><tr><td colspan="6" class="muted">Lade…</td></tr></tbody>
</table>
</div>
<p class="admin-note">Als Admin kannst du jede Projekt-Vorschau direkt öffnen – ohne Kunden-Login.</p>
</section>
<!-- Kunden -->
<section class="admin-section" id="tab-customers">
<input class="admin-search" id="customer-search" type="search"
placeholder="Kunden suchen (Kundennr., Name, E-Mail, Ort)…">
<div class="admin-table-wrap no-scroll">
<table class="admin-table">
<colgroup>
<col style="width:9%"><col style="width:23%"><col style="width:11%"><col style="width:11%">
<col style="width:13%"><col style="width:8%"><col style="width:25%">
</colgroup>
<thead>
<tr><th>Kundennr.</th><th>Kunde</th><th>Status</th><th>Portal</th><th>Letzter Login</th><th>Projekte</th><th>Aktionen</th></tr>
</thead>
<tbody id="customer-rows"><tr><td colspan="7" class="muted">Lade…</td></tr></tbody>
</table>
</div>
<p class="admin-note">Kundennummer direkt im Feld ändern und mit ✓ speichern. „Als Kunde ansehen“ zeigt dir das Portal genau so, wie der Kunde es sieht.</p>
</section>
<!-- Chat -->
<section class="admin-section" id="tab-chat">
<div class="chat-layout">
<div class="chat-side">
<input class="credit-input" id="admin-chat-new" list="admin-chat-new-list" autocomplete="off"
placeholder="Neue Nachricht an Kunde…" aria-label="Neue Nachricht an Kunde">
<datalist id="admin-chat-new-list"></datalist>
<div class="chat-list" id="admin-chat-list">
<div class="chat-list-item"><span class="muted">Lade Unterhaltungen…</span></div>
</div>
</div>
<div class="chat-thread" id="admin-chat-thread">
<div class="chat-empty">Unterhaltung links auswählen</div>
</div>
</div>
</section>
<!-- Übernahmegutschriften -->
<section class="admin-section" id="tab-credits">
<div class="credit-filter">
<button type="button" class="active" data-credit-filter="open">Offen</button>
<button type="button" data-credit-filter="all">Alle</button>
</div>
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<tr><th>Eingang</th><th>Kunde</th><th>Leistung / bisher bei</th><th>Rechnung</th><th>Bezahlt bis</th><th>Status</th><th></th></tr>
</thead>
<tbody id="credit-rows"><tr><td colspan="7" class="muted">Lade…</td></tr></tbody>
</table>
</div>
<div id="credit-detail" class="portal-card credit-detail hidden"></div>
<p class="admin-note">Übernahmegutschrift: Der Kunde hat Domain, Hosting oder E-Mail beim bisherigen Anbieter schon bezahlt. Nach der Prüfung wird der doppelt bezahlte Anteil als Guthaben in Stripe gebucht und mit den nächsten Abo-Rechnungen verrechnet.</p>
</section>
<!-- Abo-Angebote -->
<section class="admin-section" id="tab-offers">
<div class="flex-between" style="margin-bottom:12px">
<div class="credit-filter" style="margin:0">
<button type="button" class="active" data-offer-filter="open">Offen &amp; aktiv</button>
<button type="button" data-offer-filter="all">Alle</button>
</div>
<button type="button" class="mini-btn primary" id="offer-new">Neues Angebot</button>
</div>
<div id="offer-message" class="calc-result hidden"></div>
<div id="offer-form" class="portal-card hidden" style="margin-bottom:16px"></div>
<div class="admin-table-wrap no-scroll">
<table class="admin-table">
<colgroup>
<col style="width:15%"><col style="width:19%"><col style="width:33%"><col style="width:15%"><col style="width:18%">
</colgroup>
<thead>
<tr><th>Angelegt</th><th>Kunde</th><th>Angebot</th><th>Gültig bis</th><th>Status</th></tr>
</thead>
<tbody id="offer-rows"><tr><td colspan="5" class="muted">Lade…</td></tr></tbody>
</table>
</div>
<p class="admin-note">Ein Angebot legt sofort einen einmal einlösbaren Coupon in Stripe an. Ohne laufendes Abo wird er beim Abschluss angewendet (und schaltet die Bestellung für diesen Kunden frei), bei laufendem Abo gilt er ab der nächsten Abbuchung. Nach den Angebotsmonaten gilt automatisch wieder der reguläre Preis.</p>
</section>
<!-- E-Mail-Speicher (Zusatzspeicher-Aufgaben fuers Team, Hetzner-Paket) -->
<section class="admin-section" id="tab-storage">
<div id="storage-summary" class="calc-result">Lade…</div>
<div class="admin-table-wrap no-scroll">
<table class="admin-table">
<colgroup>
<col style="width:14%"><col style="width:22%"><col style="width:34%"><col style="width:18%"><col style="width:12%">
</colgroup>
<thead>
<tr><th>Fällig</th><th>Kunde</th><th>Aufgabe in konsoleH</th><th>Bestellt</th><th></th></tr>
</thead>
<tbody id="storage-rows"><tr><td colspan="5" class="muted">Lade…</td></tr></tbody>
</table>
</div>
<div id="storage-failed" class="storage-failed hidden">
<h4>Gescheiterte Änderungen (letzte 30 Tage)</h4>
<ul class="recent-list" id="storage-failed-list"></ul>
</div>
<p class="admin-note">Jedes Abo bringt 5 GB E-Mail-Speicher mit, jedes Zusatzpaket 5 GB dazu. „Einrichten“: Postfachgrenze in konsoleH jetzt auf den Zielwert setzen. „Zum Periodenende“: erst am genannten Tag auf den Zielwert senken. Die Grenzen gelten für den Gesamtspeicher des Kunden (5 GB + 5 GB je Paket); er verteilt ihn nach seiner Vorgabe auf seine Postfächer (§ 3 Abs. 4). Angezeigt wird die Domain der Kontakt-Adresse, eine eigene Mail-Domain ist im Portal nicht gespeichert. „Vergeben“ zählt, was konsoleH gerade hält: eine Verringerung zählt erst nach „Erledigt“. Danach „Erledigt“ drücken – der Kunde hat seine Bestätigung schon bekommen.</p>
</section>
</main>
<script src="/chat-composer.js"></script>
<script src="/admin.js"></script>
<script>
initAdminPage();
</script>
</body>
</html>