Files
Webklar-Kundenbereich/public/admin.html
T
ClaudeandClaude Opus 5.5 bb2f46e191 Abo: individuelle Angebote (z. B. erste 2 Monate je 25 EUR) mit Stripe-Coupon
- Admin-Tab "Abo-Angebote" und Button "Angebot" in der Kundenliste: Preis, Monate,
  Gueltigkeit, Hinweis fuer den Kunden, interne Notiz, Vorschau vor dem Anlegen
- Je Angebot sofort ein Stripe-Coupon (amount_off, repeating ueber die
  Angebotsmonate, max. 1 Einloesung); danach automatisch wieder Normalpreis
- Kunde ohne Abo: Angebot im Portal, Angebotspreis in der Bestelluebersicht,
  Coupon im Checkout; Angebot schaltet die Bestellung auch ausserhalb der
  Pilotliste frei; Bestellung prueft, dass das angezeigte Angebot noch gilt
- Laufendes Abo: Coupon sofort am Abo fuer die naechsten Abbuchungen
- Testkonten: Testrabatt im Checkout, Angebot per Webhook zusaetzlich am Abo
- Angebot in Abo-Metadaten, Vertragsbestaetigung, Admin-Mail und Abo-Karte
- Zurueckziehen loescht den Coupon; abgelaufene Angebote nicht mehr bestellbar
- Stundenkontingent nach invoice.subtotal: auch voll rabattierte oder per
  Guthaben bezahlte Monate bringen die Stunde, die Probezeit nicht
- setup-abo-collections.mjs: aboOffers, subscriptions.offerId/offerText,
  Attribut-Abfrage mit Limit (sonst Haenger ab 26 Attributen)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 20:32:08 +00:00

268 lines
16 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-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; }
/* Ü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); }
.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>
</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>
<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>
</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-list" id="admin-chat-list">
<div class="chat-list-item"><span class="muted">Lade Unterhaltungen…</span></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>
</main>
<script src="/admin.js"></script>
<script>
initAdminPage();
</script>
</body>
</html>