Rate-limit public bookings per contact; add direct owner contact endpoint
Test backoffice (smb-crm) / test (push) Successful in 1m43s
Test backoffice (smb-crm) / test (push) Successful in 1m43s
Public booking API now rejects a 6th active booking from the same customer_contact within 24h (429), stopping one contact from filling every slot on every resource, while owner-entered manual bookings stay unaffected. Add POST /api/contact: client sites can reach their own owner's inbox directly (via their existing login email) for general inquiries, separate from the agency's leads/Telegram pipeline (n8n/lead-intake.json), which stays reserved for actual prospects contacting the agency itself. Paris Barber Shop's contact form and Rückruf widget now point here; the Rückruf floating widget itself has been removed from the site. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -231,7 +231,7 @@
|
||||
<div class="wrap">
|
||||
<div class="sec-head">
|
||||
<span class="eyebrow">Kontakt</span>
|
||||
<h2>Frage stellen oder Rückruf anfordern</h2>
|
||||
<h2>Frage stellen</h2>
|
||||
<p>Schreiben Sie uns kurz — wir melden uns am selben Tag.</p>
|
||||
</div>
|
||||
<form class="lead" onsubmit="return submitLead(event, this)">
|
||||
@@ -375,8 +375,8 @@
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Lead-Formular -> n8n Webhook -> CRM (Leads) + Telegram-Benachrichtigung.
|
||||
const LEAD_WEBHOOK = 'https://n8n.mivanchenko.de/webhook/lead-intake';
|
||||
// Kontaktformular -> direkt eine E-Mail an den Inhaber (kein CRM-Lead, kein Telegram).
|
||||
const LEAD_WEBHOOK = 'https://onboard.mivanchenko.de/api/contact';
|
||||
async function submitLead(e, form) {
|
||||
e.preventDefault();
|
||||
const btn = form.querySelector('button[type=submit]');
|
||||
@@ -404,110 +404,5 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>window.CB_CTX = { client_id: "C-0004", source: "paris-barbershop-callback" };</script>
|
||||
<!-- ===== Rückruf-Widget (lead capture popup) ===== -->
|
||||
<style>
|
||||
.cb-fab { position: fixed; right: 18px; bottom: 18px; z-index: 900; background: var(--gold); color: #1a1410; border: 0; border-radius: 999px; padding: 13px 20px; font: 700 .95rem system-ui, -apple-system, Segoe UI, Roboto, sans-serif; box-shadow: 0 10px 26px rgba(200,160,90,.32); cursor: pointer; transition: transform .15s, background .2s; }
|
||||
.cb-fab:hover { transform: translateY(-2px); background: var(--gold-2); }
|
||||
.cb-overlay { position: fixed; inset: 0; z-index: 1000; background: rgba(10,8,7,.6); backdrop-filter: blur(3px); display: none; align-items: center; justify-content: center; padding: 18px; }
|
||||
.cb-overlay.open { display: flex; }
|
||||
.cb-modal { position: relative; width: min(440px, 100%); background: var(--card); color: var(--ink); border: 1px solid var(--line); border-radius: 16px; box-shadow: 0 24px 60px rgba(0,0,0,.5); padding: 28px 26px 22px; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; max-height: 92vh; overflow: auto; animation: cbIn .18s ease; }
|
||||
@keyframes cbIn { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
|
||||
.cb-modal h3 { margin: 0 0 4px; font-size: 1.35rem; font-family: 'Bebas Neue', sans-serif; letter-spacing: .5px; }
|
||||
.cb-sub { margin: 0 0 18px; color: var(--muted); font-size: .9rem; }
|
||||
.cb-x { position: absolute; top: 10px; right: 13px; background: none; border: 0; font-size: 1.7rem; line-height: 1; color: var(--muted); cursor: pointer; }
|
||||
.cb-x:hover { color: var(--ink); }
|
||||
.cb-modal label { display: block; font-size: .82rem; font-weight: 600; margin-bottom: 13px; }
|
||||
.cb-modal label .o { color: var(--muted); font-weight: 400; }
|
||||
.cb-modal input, .cb-modal textarea { width: 100%; margin-top: 5px; font: inherit; font-size: .94rem; color: var(--ink); background: #1a1611; border: 1px solid var(--line); border-radius: 9px; padding: 10px 12px; resize: vertical; }
|
||||
.cb-modal input:focus, .cb-modal textarea:focus { outline: none; border-color: var(--gold); background: #1e1a15; }
|
||||
.cb-submit { width: 100%; margin-top: 4px; background: var(--gold); color: #1a1410; border: 0; border-radius: 999px; padding: 13px; font: 700 1rem system-ui; cursor: pointer; transition: background .2s; }
|
||||
.cb-submit:hover { background: var(--gold-2); }
|
||||
.cb-submit:disabled { opacity: .6; cursor: default; }
|
||||
.cb-toast { display: none; margin-top: 14px; padding: 11px 13px; border-radius: 9px; font-size: .88rem; font-weight: 500; }
|
||||
.cb-toast.ok { background: #1e2a1c; color: #bfe6b6; border: 1px solid #2f5a2c; }
|
||||
.cb-toast.err { background: #2e1c1c; color: #f3c7c7; border: 1px solid #5a2c2c; }
|
||||
@media (max-width: 560px) { .cb-fab { right: 12px; bottom: 12px; padding: 11px 17px; } }
|
||||
</style>
|
||||
|
||||
<button class="cb-fab" type="button" onclick="cbOpen()" aria-label="Rückruf anfordern">📞 Rückruf</button>
|
||||
|
||||
<div class="cb-overlay" id="cbOverlay" onclick="cbBg(event)">
|
||||
<div class="cb-modal" role="dialog" aria-modal="true" aria-labelledby="cbTitle">
|
||||
<button class="cb-x" type="button" onclick="cbClose()" aria-label="Schließen">×</button>
|
||||
<h3 id="cbTitle">Rückruf anfordern</h3>
|
||||
<p class="cb-sub">Name & Nummer genügen — wir melden uns bei Ihnen. Alles andere ist optional.</p>
|
||||
<form onsubmit="return cbSubmit(event, this)">
|
||||
<input type="hidden" name="client_id" />
|
||||
<input type="hidden" name="source" />
|
||||
<label>Name *
|
||||
<input name="name" required autocomplete="name" placeholder="Vor- und Nachname" />
|
||||
</label>
|
||||
<label>Telefon *
|
||||
<input name="phone" required autocomplete="tel" inputmode="tel" placeholder="+49 …" />
|
||||
</label>
|
||||
<label>Gewünschte Leistung <span class="o">(optional)</span>
|
||||
<input name="service" placeholder="Herrenschnitt, Bartpflege …" />
|
||||
</label>
|
||||
<label>Nachricht <span class="o">(optional)</span>
|
||||
<textarea name="message" rows="2" placeholder="Worum geht es?"></textarea>
|
||||
</label>
|
||||
<button class="cb-submit" type="submit">Rückruf anfordern</button>
|
||||
<div class="cb-toast" id="cbToast"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const CB_WEBHOOK = 'https://n8n.mivanchenko.de/webhook/lead-intake';
|
||||
const cbCtx = window.CB_CTX || { client_id: 'PREVIEW', source: 'callback' };
|
||||
function cbOpen() {
|
||||
const o = document.getElementById('cbOverlay');
|
||||
o.querySelector('[name=client_id]').value = cbCtx.client_id;
|
||||
o.querySelector('[name=source]').value = cbCtx.source;
|
||||
o.classList.add('open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
setTimeout(() => o.querySelector('[name=name]').focus(), 60);
|
||||
}
|
||||
function cbClose() {
|
||||
document.getElementById('cbOverlay').classList.remove('open');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
function cbBg(e) { if (e.target.id === 'cbOverlay') cbClose(); }
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') cbClose(); });
|
||||
async function cbSubmit(e, form) {
|
||||
e.preventDefault();
|
||||
const btn = form.querySelector('.cb-submit');
|
||||
const toast = document.getElementById('cbToast');
|
||||
const d = Object.fromEntries(new FormData(form).entries());
|
||||
let msg = (d.message || '').trim();
|
||||
const payload = {
|
||||
client_id: d.client_id, source: d.source,
|
||||
name: d.name, phone: d.phone,
|
||||
service_interest: d.service || '', message: msg
|
||||
};
|
||||
const old = btn.textContent;
|
||||
btn.disabled = true; btn.textContent = 'Senden…';
|
||||
toast.style.display = 'none';
|
||||
try {
|
||||
const r = await fetch(CB_WEBHOOK, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
||||
if (!r.ok) throw new Error(r.status);
|
||||
toast.className = 'cb-toast ok';
|
||||
toast.textContent = '✓ Danke! Wir rufen Sie zurück.';
|
||||
toast.style.display = 'block';
|
||||
form.reset();
|
||||
setTimeout(cbClose, 2200);
|
||||
} catch (err) {
|
||||
toast.className = 'cb-toast err';
|
||||
toast.textContent = '⚠ Senden fehlgeschlagen. Bitte erneut versuchen.';
|
||||
toast.style.display = 'block';
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = old;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
<!-- ===== /Rückruf-Widget ===== -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user