f1577bcefa
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>
231 lines
12 KiB
HTML
231 lines
12 KiB
HTML
<!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> & <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 & 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 & meldet per Telegram — kann ~1–4 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 1–4 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>
|