Files
Key-service-Server/Website/templates/schnell_buchung.html
T

562 lines
21 KiB
HTML

{% extends "base.html" %}
{% block title %}Buchung prüfen & abschließen{% endblock %}
{% block head %}
{{ super() }}
<style>
.booking-review {
max-width: 920px;
margin: 2rem auto 3rem;
padding: 0;
}
.booking-review__panel {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(244, 247, 244, 0.96));
border: 1px solid rgba(28, 90, 99, 0.14);
border-radius: 26px;
padding: clamp(1.4rem, 2.6vw, 2.2rem);
box-shadow: 0 18px 44px rgba(18, 49, 54, 0.08);
position: relative;
overflow: hidden;
}
.booking-review__eyebrow {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.42rem 0.82rem;
border-radius: 999px;
background: #e8f4f4;
color: #166271;
font-weight: 700;
margin-bottom: 1rem;
}
.booking-review__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
margin: 1.5rem 0;
}
.booking-review__card {
background: #ffffff;
border: 1px solid #dbe6e7;
border-radius: 18px;
padding: 1rem 1.1rem;
}
.booking-review__label {
display: block;
font-size: 0.88rem;
font-weight: 700;
text-transform: uppercase;
color: #5e757d;
margin-bottom: 0.45rem;
}
.booking-review__value {
color: #173a42;
font-weight: 700;
line-height: 1.45;
}
.booking-review__note {
padding: 1rem 1.1rem;
border-radius: 18px;
background: #f8fbfb;
border: 1px solid #dbe6e7;
color: #49626b;
line-height: 1.65;
}
/* Formular Styles */
.booking-review__form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.9rem;
margin-top: 1.25rem;
}
.booking-review__section-title {
grid-column: 1 / -1;
font-size: 1.15rem;
color: #166271;
margin-top: 1.5rem;
margin-bottom: 0.2rem;
border-bottom: 2px solid #e8f4f4;
padding-bottom: 0.5rem;
font-weight: 700;
}
.booking-review__field {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.booking-review__field--full {
grid-column: 1 / -1;
}
.booking-review__field label {
font-weight: 700;
color: #2b4f58;
font-size: 0.93rem;
}
.booking-review__field input {
border: 1px solid #cedbdd;
border-radius: 12px;
background: #ffffff;
color: #173a42;
padding: 0.75rem 0.82rem;
}
.input-group--subdomain {
display: flex;
align-items: center;
}
.input-group--subdomain input {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: none;
flex-grow: 1;
}
.input-group--subdomain .domain-suffix {
background: #e8f4f4;
border: 1px solid #cedbdd;
padding: 0.75rem 0.82rem;
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
color: #5e757d;
font-weight: 600;
}
.booking-review__consent {
padding: 0.95rem 1rem;
border-radius: 16px;
background: #eef6f6;
border: 1px solid #d2e2e2;
}
.booking-review__consent label {
display: flex;
gap: 0.7rem;
align-items: flex-start;
font-weight: 600;
color: #21414a;
line-height: 1.5;
}
.booking-review__consent input {
margin-top: 0.2rem;
}
.booking-review__actions {
display: flex;
flex-wrap: wrap;
gap: 0.85rem;
margin-top: 1.5rem;
}
/* Loading State UI */
#setup-loading-state,
#setup-success-state {
display: none;
text-align: center;
padding: 3rem 1rem;
}
.spinner {
width: 60px;
height: 60px;
border: 5px solid #e8f4f4;
border-top-color: #166271;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 2rem;
}
@keyframes spin { 100% { transform: rotate(360deg); } }
.status-messages {
font-size: 1.1rem;
color: #49626b;
font-weight: 600;
height: 2rem;
}
/* Success State UI */
.success-icon {
width: 70px;
height: 70px;
background: #1b8a3e;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
margin: 0 auto 1.5rem;
}
.credentials-box {
background: #173a42;
color: white;
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem auto;
max-width: 450px;
text-align: left;
}
.credentials-box code {
display: block;
background: rgba(255,255,255,0.1);
padding: 0.5rem;
border-radius: 6px;
margin-top: 0.3rem;
font-size: 1.1rem;
user-select: all;
}
.warning-text {
color: #b02a37;
font-weight: 700;
background: #f8d7da;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1.5rem;
}
.btn.success-btn {
background-color: #1b8a3e;
border-color: #1b8a3e;
color: #fff;
}
.btn.success-btn:hover {
background-color: #146c2e;
border-color: #146c2e;
}
@media (max-width: 720px) {
.booking-review__grid, .booking-review__form-grid { grid-template-columns: 1fr; }
.booking-review__actions { flex-direction: column; }
.booking-review__actions .btn { width: 100%; min-width: 0; }
}
/* Verstecke UI-Elemente beim Drucken */
@media print {
header, footer, nav, .booking-review__actions { display: none !important; }
body { background: white; }
.booking-review__panel { box-shadow: none; border: none; padding: 0; }
}
</style>
{% endblock %}
{% block content %}
<section class="booking-review">
<div class="booking-review__panel">
<!-- ========================================== -->
<!-- STATE 1: FORMULAR (Checkout) -->
<!-- ========================================== -->
<div id="checkout-form-state">
<span class="booking-review__eyebrow">Buchung prüfen & abschließen</span>
<h1>{{ title }}</h1>
<div class="booking-review__grid">
<div class="booking-review__card">
<span class="booking-review__label">Paket</span>
<div class="booking-review__value">{{ package }}</div>
</div>
<!-- OPTIMIERT: Eindeutiger Hinweis auf Rechnungsstellung -->
<div class="booking-review__card" style="border-left: 4px solid #1b8a3e;">
<span class="booking-review__label">Zahlungsart</span>
<div class="booking-review__value" style="color: #1b8a3e;">Kauf auf Rechnung</div>
<small style="color: #5e757d; font-weight: 600;">30 Tage Zahlungsziel nach Bereitstellung</small>
</div>
</div>
<div class="booking-review__note">
Die Software-Instanz wird nach dem Absenden <strong>sofort für 30 Tage freigeschaltet</strong>. Die finale Laufzeit aktiviert sich automatisch nach Zahlungseingang durch Ihren Schulträger.
</div>
<form id="checkout-form" method="POST" action="{{ url_for('booking_payment_async') }}">
<input type="hidden" name="package" value="{{ package_key }}">
<div class="booking-review__form-grid">
<div class="booking-review__section-title">1. Angaben zur Schule & Ansprechpartner</div>
<div class="booking-review__field">
<label for="school_name">Name der Schule *</label>
<input id="school_name" name="school_name" type="text" required value="{{ form_data.school_name }}">
</div>
<div class="booking-review__field">
<label for="school_number">Dienststellennummer *</label>
<input id="school_number" name="school_number" type="text" required placeholder="Zur Legitimation" value="{{ form_data.school_number }}">
</div>
<div class="booking-review__field">
<label for="contact_person">Ansprechpartner *</label>
<input id="contact_person" name="contact_person" type="text" required value="{{ form_data.contact_person }}">
</div>
<div class="booking-review__field">
<label for="contact_email">Dienstliche E-Mail *</label>
<input id="contact_email" name="contact_email" type="email" required value="{{ form_data.contact_email }}">
</div>
<!-- OPTIMIERT: Rechnungsdaten inkl. Kassenzeichen -->
<div class="booking-review__section-title">2. Rechnungsadresse (Schulträger)</div>
<div class="booking-review__field booking-review__field--full">
<label for="billing_organization">Einrichtung / Amt *</label>
<input id="billing_organization" name="billing_organization" type="text" required placeholder="z.B. Stadt Neustadt - Schulverwaltungsamt" value="{{ form_data.billing_organization }}">
</div>
<div class="booking-review__field booking-review__field--full">
<label for="billing_street">Straße & Hausnummer *</label>
<input id="billing_street" name="billing_street" type="text" required value="{{ form_data.billing_street }}">
</div>
<div class="booking-review__field">
<label for="billing_zip">PLZ *</label>
<input id="billing_zip" name="billing_zip" type="text" required value="{{ form_data.billing_zip }}">
</div>
<div class="booking-review__field">
<label for="billing_city">Ort *</label>
<input id="billing_city" name="billing_city" type="text" required value="{{ form_data.billing_city }}">
</div>
<div class="booking-review__field booking-review__field--full">
<label for="billing_reference">Kassenzeichen / Haushaltsstelle / Aktenzeichen <span style="font-weight: normal; color: #5e757d;">(Optional)</span></label>
<input id="billing_reference" name="billing_reference" type="text" placeholder="Wichtig für die Zahlungsabwicklung der Stadt" value="{{ form_data.billing_reference }}">
</div>
<div class="booking-review__section-title">3. Technische Einrichtung</div>
<div class="booking-review__field booking-review__field--full">
<label for="tenant_slug">Gewünschte Schul-URL *</label>
<div class="input-group--subdomain">
<input id="tenant_slug" name="tenant_slug" type="text" required pattern="[a-z0-9-]+" title="Nur Kleinbuchstaben, Zahlen und Bindestriche erlaubt" placeholder="gymnasium-neustadt" value="{{ form_data.tenant_slug }}">
<span class="domain-suffix">.invario-software.de</span>
</div>
<div id="tenant_slug_feedback" style="margin-top:0.4rem; font-size:0.9rem; font-weight:600;"></div>
</div>
<!-- OPTIMIERT: Rechtsgültige Links für Verträge -->
<div class="booking-review__field booking-review__field--full booking-review__consent">
<label for="agreements_accepted">
<input id="agreements_accepted" name="agreements_accepted" type="checkbox" required>
<span>
Ich habe die <a href="{{ url_for('nutzungsbedingungen') }}" target="_blank" rel="noopener">AGB (PDF)</a>,
die <a href="{{ url_for('datenschutz') }}" target="_blank" rel="noopener">Datenschutzerklärung</a> sowie den gesetzlich erforderlichen
<a href="{{ url_for('avv') }}" target="_blank" rel="noopener">Auftragsverarbeitungsvertrag (AVV)</a> gelesen und stimme diesen Dokumenten verbindlich zu.
</span>
</label>
</div>
</div>
<!-- Fehleranzeige, falls der Server beim Submit meckert -->
<div id="form-error-alert" style="display:none; color:#b02a37; margin-top:1rem; font-weight:bold;"></div>
<div class="booking-review__actions">
<button id="booking_submit" class="btn" type="submit">Kostenpflichtig buchen & Instanz starten</button>
</div>
</form>
</div>
<!-- ========================================== -->
<!-- STATE 2: LOADING (Asynchrones Setup) -->
<!-- ========================================== -->
<div id="setup-loading-state">
<h2>Instanz wird bereitgestellt</h2>
<p>Ihre Bestellung wird verarbeitet. Die Rechnung wird zeitgleich generiert.</p>
<div class="spinner"></div>
<div class="status-messages" id="loading-status-text">Validierung der Daten...</div>
<p style="margin-top: 2rem; color: #5e757d; font-size: 0.9rem;">Bitte schließen Sie dieses Fenster nicht. Dies dauert in der Regel 15-30 Sekunden.</p>
</div>
<!-- ========================================== -->
<!-- STATE 3: SUCCESS (Bereit zur Nutzung) -->
<!-- ========================================== -->
<div id="setup-success-state">
<div class="success-icon">✓</div>
<h2>Ihre Schule ist online!</h2>
<p>Das Setup wurde erfolgreich abgeschlossen. Ihre Rechnung wurde an die angegebene E-Mail-Adresse versandt.</p>
<div class="warning-text">
Aus Datenschutzgründen (AVV) müssen Sie das generierte Passwort nach dem ersten Login zwingend ändern!
</div>
<div class="credentials-box">
<p><strong>Login-URL:</strong> <br><a id="success-url" href="#" target="_blank" style="color: #66d9e8;">-</a></p>
<p><strong>Benutzername (Admin):</strong> <code id="success-user">-</code></p>
<p><strong>Initial-Passwort:</strong> <code id="success-pass">-</code></p>
</div>
<!-- OPTIMIERT: Rechnungs-Download, Weiterleitung und Drucken -->
<div class="booking-review__actions" style="justify-content: center; margin-top: 2rem;">
<a id="btn-download-invoice" href="#" class="btn success-btn" target="_blank" style="display: none;">
📄 Rechnung (PDF) herunterladen
</a>
<a id="btn-go-to-tenant" href="#" class="btn" target="_blank">Zur Schulplattform wechseln</a>
<button onclick="window.print();" class="btn secondary" type="button">
🖨️ Zugangsdaten drucken
</button>
</div>
</div>
</div>
</section>
<script>
(() => {
// DOM Elemente
const formState = document.getElementById('checkout-form-state');
const loadingState = document.getElementById('setup-loading-state');
const successState = document.getElementById('setup-success-state');
const form = document.getElementById('checkout-form');
const errorAlert = document.getElementById('form-error-alert');
const statusText = document.getElementById('loading-status-text');
// Subdomain Live-Prüfung
const slugInput = document.getElementById('tenant_slug');
const feedback = document.getElementById('tenant_slug_feedback');
const submitBtn = document.getElementById('booking_submit');
let slugTimer = null;
slugInput.addEventListener('input', (e) => {
e.target.value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '');
});
slugInput.addEventListener('keyup', (e) => {
clearTimeout(slugTimer);
slugTimer = setTimeout(() => checkSubdomain(e.target.value), 600);
});
async function checkSubdomain(name) {
if (name.length < 3) return;
try {
const res = await fetch(`/instance_request/check_subdomain?slug=${name}`);
const j = await res.json();
if (j.available) {
feedback.textContent = 'Adresse ist frei!';
feedback.style.color = '#1b8a3e';
submitBtn.disabled = false;
} else {
feedback.textContent = 'Adresse ist bereits vergeben.';
feedback.style.color = '#b02a37';
submitBtn.disabled = true;
}
} catch (e) {
console.error(e);
}
}
// ==========================================
// AJAX Form Submit & Transition to Loading
// ==========================================
form.addEventListener('submit', async (e) => {
e.preventDefault();
errorAlert.style.display = 'none';
submitBtn.disabled = true;
const formData = new FormData(form);
const tenantSlug = formData.get('tenant_slug');
try {
const response = await fetch(form.action, {
method: 'POST',
body: formData,
headers: { 'Accept': 'application/json' }
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || 'Es gab ein Problem bei der Buchung.');
}
// Erfolg: Formular verstecken, Ladebildschirm zeigen
formState.style.display = 'none';
loadingState.style.display = 'block';
// Starte Polling
startPolling(tenantSlug);
} catch (error) {
errorAlert.textContent = error.message;
errorAlert.style.display = 'block';
submitBtn.disabled = false;
}
});
// ==========================================
// Polling Logik (Warten auf den Server)
// ==========================================
async function startPolling(slug) {
const statusMessages = [
"Registriere Schuldatenbank...",
"Erstelle Auftragsverarbeitungsvertrag...",
"Generiere Rechnung für Schulträger...",
"Konfiguriere Nginx & Zertifikate...",
"Führe finale Health-Checks durch..."
];
let msgIndex = 0;
const msgInterval = setInterval(() => {
if (msgIndex < statusMessages.length) {
statusText.textContent = statusMessages[msgIndex];
msgIndex++;
}
}, 3500);
const checkStatus = async () => {
try {
const res = await fetch(`/instance_request/status?slug=${slug}`);
const data = await res.json();
if (data.status === 'ready') {
// SETUP FERTIG!
clearInterval(msgInterval);
showSuccessState(data.url, data.username, data.password, data.invoice_url);
} else if (data.status === 'failed') {
// SETUP FEHLGESCHLAGEN
clearInterval(msgInterval);
statusText.textContent = "Fehler bei der Bereitstellung. Bitte Support kontaktieren.";
statusText.style.color = '#b02a37';
} else {
// SETUP LÄUFT NOCH -> in 3 Sekunden erneut prüfen
setTimeout(checkStatus, 3000);
}
} catch (err) {
setTimeout(checkStatus, 3000);
}
};
// Starte den ersten Check nach 2 Sekunden
setTimeout(checkStatus, 2000);
}
// ==========================================
// Erfolgs-Bildschirm anzeigen
// ==========================================
function showSuccessState(url, user, pass, invoiceUrl) {
loadingState.style.display = 'none';
successState.style.display = 'block';
document.getElementById('success-url').textContent = url;
document.getElementById('success-url').href = url;
document.getElementById('success-user').textContent = user;
document.getElementById('success-pass').textContent = pass;
document.getElementById('btn-go-to-tenant').href = url;
// Rechnungs-Button einblenden, falls URL vorhanden
const invoiceBtn = document.getElementById('btn-download-invoice');
if (invoiceUrl) {
invoiceBtn.href = invoiceUrl;
invoiceBtn.style.display = 'inline-flex';
}
}
})();
</script>
{% endblock %}