Files
smb-online/templates/onboarding/index.html
T
mivanchenko f1577bcefa Phase 5: add internal operator onboarding page
Self-contained admin form that POSTs to the /webhook/onboard endpoint
(creates Clients + Projects + Activity Log rows). Hosted behind HTTP
basic auth at onboard.mivanchenko.de (Caddy basic_auth -> smb-demos
container). Not linked from the public demos; creates billable records
so it stays operator-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 08:00:36 +02:00

231 lines
12 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.0" />
<meta name="robots" content="noindex, nofollow" />
<title>Kunden-Onboarding · SMB-Online (intern)</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #f6f8f8; --surface: #ffffff; --ink: #16302f; --muted: #5d716f;
--teal: #0f8a7e; --teal-2: #0b6b62; --teal-soft: #e4f3f1; --line: #e3eae9;
--radius: 14px; --shadow: 0 16px 44px rgba(16,48,47,.10);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background: var(--bg); color: var(--ink); line-height: 1.6; -webkit-font-smoothing: antialiased; padding: 0 0 80px; }
h1, h2 { font-family: 'Fraunces', Georgia, serif; font-weight: 600; line-height: 1.15; }
.topbar { background: #0c2b29; color: #9fe6dc; font-size: .76rem; letter-spacing: 1px; text-align: center; padding: 7px 12px; text-transform: uppercase; }
.wrap { width: min(820px, 92vw); margin: 36px auto 0; }
.head { display: flex; align-items: center; gap: 11px; margin-bottom: 6px; }
.logo { width: 34px; height: 34px; border-radius: 9px; background: linear-gradient(135deg, var(--teal), var(--teal-2)); display: grid; place-items: center; color: #fff; font-size: 1.05rem; font-weight: 700; }
.head h1 { font-size: 1.7rem; }
.sub { color: var(--muted); font-size: .95rem; margin-bottom: 26px; }
.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 26px 28px; }
fieldset { border: 0; margin-bottom: 22px; }
legend { font-family: 'Inter'; font-size: .74rem; text-transform: uppercase; letter-spacing: 1.4px; color: var(--teal-2); font-weight: 700; margin-bottom: 14px; width: 100%; border-bottom: 1px solid var(--line); padding-bottom: 7px; }
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 14px; }
.row.one { grid-template-columns: 1fr; }
.field { display: flex; flex-direction: column; gap: 5px; }
label { font-size: .82rem; font-weight: 600; color: var(--ink); }
label .opt { color: var(--muted); font-weight: 400; }
input, select, textarea { font-family: inherit; font-size: .94rem; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: 9px; padding: 10px 12px; transition: border-color .15s, background .15s; }
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--teal); background: #fff; }
textarea { resize: vertical; min-height: 64px; }
.hint { font-size: .76rem; color: var(--muted); }
.tier-toggle { display: flex; gap: 10px; }
.tier-toggle label { flex: 1; cursor: pointer; border: 1px solid var(--line); border-radius: 10px; padding: 11px 13px; background: var(--bg); display: flex; flex-direction: column; gap: 2px; transition: all .15s; }
.tier-toggle input { display: none; }
.tier-toggle label:has(input:checked) { border-color: var(--teal); background: var(--teal-soft); }
.tier-toggle b { font-size: .9rem; }
.tier-toggle span { font-size: .74rem; color: var(--muted); font-weight: 400; }
.preview { background: var(--teal-soft); border: 1px dashed var(--teal); border-radius: 10px; padding: 13px 15px; font-size: .84rem; color: var(--teal-2); margin-bottom: 20px; }
.preview b { font-weight: 700; }
.actions { display: flex; align-items: center; gap: 16px; margin-top: 6px; }
.btn { background: var(--teal); color: #fff; font-weight: 600; padding: 13px 26px; border-radius: 999px; border: 0; cursor: pointer; font-size: .98rem; transition: transform .15s, background .2s; }
.btn:hover { transform: translateY(-2px); background: var(--teal-2); }
.btn:disabled { opacity: .6; cursor: default; transform: none; }
.toast { display: none; padding: 13px 16px; border-radius: 10px; font-size: .9rem; font-weight: 500; margin-top: 18px; }
.toast.ok { background: #e3f6ec; color: #0c6b3c; border: 1px solid #b8e6cc; }
.toast.err { background: #fdecec; color: #9b2226; border: 1px solid #f3c7c7; }
@media (max-width: 620px) { .row { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<div class="topbar">⚙ Interne Seite — Kunden anlegen · nicht öffentlich teilen</div>
<div class="wrap">
<div class="head">
<div class="logo"></div>
<h1>Kunden-Onboarding</h1>
</div>
<p class="sub">Neuen Kunden ins CRM aufnehmen. Erzeugt Zeilen in <b>Clients</b>, <b>Projects</b> und <b>Activity Log</b>. <code>client_id</code>, <code>renewal_date</code> &amp; <code>created_at</code> werden automatisch vergeben.</p>
<div class="card">
<form id="onboard" onsubmit="return submitOnboard(event, this)">
<fieldset>
<legend>Betrieb</legend>
<div class="row">
<div class="field">
<label for="business_name">Firmenname *</label>
<input id="business_name" name="business_name" required placeholder="Café Lichtblick" />
</div>
<div class="field">
<label for="owner_name">Inhaber:in <span class="opt">(optional)</span></label>
<input id="owner_name" name="owner_name" placeholder="Sara Hell" />
</div>
</div>
<div class="row">
<div class="field">
<label for="email">E-Mail <span class="opt">(optional)</span></label>
<input id="email" name="email" type="email" placeholder="sara@cafe-lichtblick.de" />
</div>
<div class="field">
<label for="phone">Telefon <span class="opt">(optional)</span></label>
<input id="phone" name="phone" placeholder="+49 911 445566" />
</div>
</div>
<div class="row">
<div class="field">
<label for="niche">Branche <span class="opt">(optional)</span></label>
<input id="niche" name="niche" placeholder="Café" />
</div>
<div class="field">
<label for="domain">Domain <span class="opt">(optional)</span></label>
<input id="domain" name="domain" placeholder="cafe-lichtblick.de" />
</div>
</div>
</fieldset>
<fieldset>
<legend>Leistung &amp; Abrechnung</legend>
<div class="row one">
<div class="field">
<label>Tier</label>
<div class="tier-toggle">
<label><input type="radio" name="tier" value="A" checked /><b>Tier A</b><span>Google-Stack · €0 Infra</span></label>
<label><input type="radio" name="tier" value="B" /><b>Tier B</b><span>Self-hosted · Datenschutz</span></label>
</div>
</div>
</div>
<div class="row one">
<div class="field">
<label for="services">Services <span class="opt">(Komma-getrennt)</span></label>
<input id="services" name="services" value="site, booking, leads" />
<span class="hint">Wird zur Projekt-Checkliste: jedes Wort wird ein Häkchen.</span>
</div>
</div>
<div class="row">
<div class="field">
<label for="billing_cycle">Abrechnung</label>
<select id="billing_cycle" name="billing_cycle" onchange="updatePreview()">
<option value="monthly">monatlich</option>
<option value="yearly">jährlich</option>
<option value="one-off">einmalig</option>
</select>
</div>
<div class="field">
<label for="monthly_fee_eur">Gebühr (€) <span class="opt">(optional)</span></label>
<input id="monthly_fee_eur" name="monthly_fee_eur" type="number" min="0" step="1" placeholder="35" />
</div>
</div>
<div class="row one">
<div class="field">
<label for="start_date">Startdatum <span class="opt">(leer = heute)</span></label>
<input id="start_date" name="start_date" type="date" onchange="updatePreview()" />
</div>
</div>
</fieldset>
<fieldset>
<legend>Intern</legend>
<div class="row">
<div class="field">
<label for="stack_notes">Stack-Notizen <span class="opt">(optional)</span></label>
<input id="stack_notes" name="stack_notes" placeholder="Cloudflare Pages + Google Kalender" />
</div>
<div class="field">
<label for="vault_ref">Vault-Referenz <span class="opt">(optional)</span></label>
<input id="vault_ref" name="vault_ref" placeholder="VW-0002" />
</div>
</div>
<div class="row one">
<div class="field">
<label for="notes">Notizen <span class="opt">(optional)</span></label>
<textarea id="notes" name="notes" placeholder="Sonstiges …"></textarea>
</div>
</div>
</fieldset>
<div class="preview" id="preview"></div>
<div class="actions">
<button class="btn" type="submit">Kunde anlegen</button>
<span class="hint">Schreibt ins CRM &amp; meldet per Telegram — kann ~14 Min dauern.</span>
</div>
<div class="toast" id="toast"></div>
</form>
</div>
</div>
<script>
const ONBOARD_WEBHOOK = 'https://n8n.mivanchenko.de/webhook/onboard';
function addMonths(d, n) { const x = new Date(d); x.setUTCMonth(x.getUTCMonth() + n); return x; }
function addYears(d, n) { const x = new Date(d); x.setUTCFullYear(x.getUTCFullYear() + n); return x; }
function addDays(d, n) { const x = new Date(d); x.setUTCDate(x.getUTCDate() + n); return x; }
function iso(d) { return d.toISOString().slice(0, 10); }
function updatePreview() {
const sd = document.getElementById('start_date').value;
const cycle = document.getElementById('billing_cycle').value;
const start = sd ? new Date(sd + 'T00:00:00Z') : new Date();
let renewal = '—';
if (cycle === 'monthly') renewal = iso(addMonths(start, 1));
else if (cycle === 'yearly') renewal = iso(addYears(start, 1));
const goLive = iso(addDays(start, 14));
document.getElementById('preview').innerHTML =
'Auto-berechnet → Start: <b>' + iso(start) + '</b> · Verlängerung: <b>' + renewal +
'</b> · Go-Live-Ziel: <b>' + goLive + '</b> · client_id wird fortlaufend vergeben.';
}
async function submitOnboard(e, form) {
e.preventDefault();
const btn = form.querySelector('button[type=submit]');
const toast = document.getElementById('toast');
const data = Object.fromEntries(new FormData(form).entries());
const old = btn.textContent;
btn.disabled = true; btn.textContent = 'Wird angelegt …';
toast.style.display = 'none';
try {
const r = await fetch(ONBOARD_WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!r.ok) throw new Error(r.status);
toast.className = 'toast ok';
toast.textContent = '✓ Kunde übermittelt. Zeilen erscheinen in 14 Min im Sheet, Telegram-Meldung folgt.';
toast.style.display = 'block';
form.reset();
document.querySelector('input[name=tier][value=A]').checked = true;
document.getElementById('services').value = 'site, booking, leads';
updatePreview();
} catch (err) {
toast.className = 'toast err';
toast.textContent = '⚠ Übermittlung fehlgeschlagen (' + err.message + '). Nochmal versuchen.';
toast.style.display = 'block';
} finally {
btn.disabled = false; btn.textContent = old;
}
return false;
}
updatePreview();
</script>
</body>
</html>