Slight changes with the checkout system

This commit is contained in:
2026-07-19 12:47:10 +02:00
parent d6ed54ab02
commit 818773e459
2 changed files with 375 additions and 131 deletions
+20
View File
@@ -3406,6 +3406,26 @@ def logout():
flash('Logged out successfully', 'info')
return redirect(url_for('login'))
@app.route('/booking_payment_async', methods=['POST'])
def booking_payment_async():
# ... Speichere Daten in DB ...
# ... Starte das manage-tenants.sh Skript ASYNCHRON (z.B. Celery) ...
return jsonify({"success": True, "tenant_slug": request.form['tenant_slug']}), 200
@app.route('/instance_request/status')
def tenant_status():
slug = request.args.get('slug')
# Prüfe in der Datenbank, ob das Skript für diesen Slug schon fertig ist
if ist_fertig(slug):
return jsonify({
"status": "ready",
"url": f"https://{slug}.invario-software.de",
"username": "admin",
"password": generiertes_passwort
})
else:
return jsonify({"status": "pending"})
def main():
app.run(host="0.0.0.0", port=4999, debug=False)
+327 -103
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Buchung prüfen{% endblock %}
{% block title %}Buchung prüfen & abschließen{% endblock %}
{% block head %}
{{ super() }}
@@ -17,6 +17,8 @@
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 {
@@ -31,11 +33,6 @@
margin-bottom: 1rem;
}
.booking-review__lead {
margin-top: 0.75rem;
max-width: 66ch;
}
.booking-review__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -54,7 +51,6 @@
display: block;
font-size: 0.88rem;
font-weight: 700;
letter-spacing: 0.03em;
text-transform: uppercase;
color: #5e757d;
margin-bottom: 0.45rem;
@@ -75,6 +71,7 @@
line-height: 1.65;
}
/* Formular Styles */
.booking-review__form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -82,6 +79,17 @@
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;
@@ -98,8 +106,7 @@
font-size: 0.93rem;
}
.booking-review__field input,
.booking-review__field textarea {
.booking-review__field input {
border: 1px solid #cedbdd;
border-radius: 12px;
background: #ffffff;
@@ -107,9 +114,26 @@
padding: 0.75rem 0.82rem;
}
.booking-review__field textarea {
min-height: 110px;
resize: vertical;
.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 {
@@ -130,13 +154,6 @@
.booking-review__consent input {
margin-top: 0.2rem;
width: auto;
flex: 0 0 auto;
}
.booking-review__consent a {
color: #0a4c74;
text-decoration: underline;
}
.booking-review__actions {
@@ -146,26 +163,78 @@
margin-top: 1.5rem;
}
.booking-review__actions .btn,
.booking-review__actions a {
width: auto;
min-width: 190px;
/* NEU: 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;
}
/* NEU: 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: 400px;
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;
}
@media (max-width: 720px) {
.booking-review__grid {
grid-template-columns: 1fr;
}
.booking-review__form-grid {
grid-template-columns: 1fr;
}
.booking-review__actions .btn,
.booking-review__actions a {
width: 100%;
min-width: 0;
}
.booking-review__grid, .booking-review__form-grid { grid-template-columns: 1fr; }
}
</style>
{% endblock %}
@@ -173,123 +242,278 @@
{% block content %}
<section class="booking-review">
<div class="booking-review__panel">
<span class="booking-review__eyebrow">Buchung prüfen</span>
<h1>{{ title }}</h1>
<p class="booking-review__lead">{{ intro }}</p>
<div class="booking-review__grid" aria-label="Buchungsübersicht">
<!-- ========================================== -->
<!-- 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>
<div class="booking-review__card">
<span class="booking-review__label">Preis</span>
<div class="booking-review__value">{{ package_price }} €</div>
</div>
<div class="booking-review__card">
<span class="booking-review__label">Angemeldet als</span>
<div class="booking-review__value">{{ display_name }}</div>
<div class="booking-review__value">{{ package_price }} € <br><small>Kauf auf Rechnung (30 Tage)</small></div>
</div>
</div>
<div class="booking-review__note">
Bitte prüfen Sie die Angaben noch einmal. Wenn alles korrekt ist, senden sie ihre Informationen mit dem folgenden Button an uns, wir senden ihnen frühstmöglich eine Rechnung per Email.
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 method="POST" action="{{ url_for('booking_payment') }}">
<!-- Das Formular schickt die Daten an /booking_payment_async -->
<form id="checkout-form" method="POST" action="{{ url_for('booking_payment_async') }}">
<input type="hidden" name="package" value="{{ package_key }}">
<input type="hidden" name="flow" value="{{ flow }}">
<div class="booking-review__form-grid" aria-label="Pflichtdaten für Buchung">
<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 maxlength="120" value="{{ form_data.contact_person }}">
<input id="contact_person" name="contact_person" type="text" required value="{{ form_data.contact_person }}">
</div>
<div class="booking-review__field">
<label for="school_name">Schule/Einrichtung *</label>
<input id="school_name" name="school_name" type="text" required maxlength="160" value="{{ form_data.school_name }}">
<div id="school_name_feedback" style="margin-top:0.4rem; font-size:0.9rem; color:#6b6b6b;"></div>
<label for="contact_email">Dienstliche E-Mail *</label>
<input id="contact_email" name="contact_email" type="email" required value="{{ form_data.contact_email }}">
</div>
<div class="booking-review__field">
<label for="contact_email">E-Mail *</label>
<input id="contact_email" name="contact_email" type="email" required maxlength="160" value="{{ form_data.contact_email }}">
</div>
<div class="booking-review__field">
<label for="contact_phone">Telefon</label>
<input id="contact_phone" name="contact_phone" type="text" maxlength="60" value="{{ form_data.contact_phone }}">
<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 value="{{ form_data.billing_organization }}">
</div>
<div class="booking-review__field booking-review__field--full">
<label for="billing_street">Straße *</label>
<input id="billing_street" name="billing_street" type="text" required maxlength="160" value="{{ form_data.billing_street }}">
<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 maxlength="20" value="{{ form_data.billing_zip }}">
<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 maxlength="120" value="{{ form_data.billing_city }}">
<input id="billing_city" name="billing_city" type="text" required value="{{ form_data.billing_city }}">
</div>
<div class="booking-review__section-title">3. Technische Einrichtung</div>
<div class="booking-review__field booking-review__field--full">
<label for="notes">Zusatzinformationen</label>
<textarea id="notes" name="notes" maxlength="600" placeholder="Optional: gewünschter Starttermin, besondere Anforderungen, Rückrufzeiten">{{ form_data.notes }}</textarea>
<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-]+" placeholder="gymnasium-neustadt" value="{{ form_data.tenant_slug }}">
<span class="domain-suffix">.euresoftware.de</span>
</div>
<div id="tenant_slug_feedback" style="margin-top:0.4rem; font-size:0.9rem; font-weight:600;"></div>
</div>
<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" value="yes" required {% if form_data.agreements_accepted %}checked{% endif %}>
<span>Ich habe die <a href="{{ url_for('nutzungsbedingungen') }}" target="_blank" rel="noopener">Lizenzvereinbarung</a>, die <a href="{{ url_for('datenschutz') }}" target="_blank" rel="noopener">Datenschutzerklärung</a> und die DSGVO-Hinweise gelesen und akzeptiere sie.</span>
<input id="agreements_accepted" name="agreements_accepted" type="checkbox" required>
<span>Ich habe die Nutzungsbedingungen und den obligatorischen AVV gelesen und stimme diesen 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">{{ confirm_label }}</button>
<a class="btn secondary" href="{{ url_for('preise') }}">Zurück zu den Preisen</a>
<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 war erfolgreich. Die Rechnung wird gerade verschickt.</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. Sie können sich nun einloggen.</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>
<a id="btn-go-to-tenant" href="#" class="btn" target="_blank" style="width: 100%; max-width: 400px;">Jetzt zur Schulplattform wechseln</a>
</div>
</div>
</section>
<script>
(() => {
const input = document.getElementById('school_name');
const feedback = document.getElementById('school_name_feedback');
const submit = document.getElementById('booking_submit');
let timer = null;
// DOM Elemente
const formState = document.getElementById('checkout-form-state');
const loadingState = document.getElementById('setup-loading-state');
const successState = document.getElementById('setup-success-state');
function setFeedback(text, ok) {
feedback.textContent = text;
feedback.style.color = ok ? '#1b8a3e' : '#b02a37';
submit.disabled = !ok;
}
const form = document.getElementById('checkout-form');
const errorAlert = document.getElementById('form-error-alert');
const statusText = document.getElementById('loading-status-text');
async function check(name) {
if (!name || name.trim().length < 3) {
feedback.textContent = '';
submit.disabled = false;
return;
}
try {
const params = new URLSearchParams({ school_name: name });
const res = await fetch(`/instance_request/check_subdomain?${params.toString()}`, { credentials: 'same-origin' });
if (!res.ok) throw new Error('Netzwerkfehler');
const j = await res.json();
setFeedback(j.message || (j.available ? 'verfügbar' : 'nicht verfügbar'), !!j.available);
} catch (e) {
setFeedback('Prüfung fehlgeschlagen (Netzwerk).', false);
}
}
// 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;
if (input) {
input.addEventListener('input', (e) => {
clearTimeout(timer);
const val = e.target.value;
timer = setTimeout(() => check(val), 500);
slugInput.addEventListener('input', (e) => {
e.target.value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '');
});
// initial check on load
if (input.value && input.value.trim()) {
check(input.value);
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(); // Verhindert das klassische Neuladen der Seite
errorAlert.style.display = 'none';
submitBtn.disabled = true;
const formData = new FormData(form);
const tenantSlug = formData.get('tenant_slug');
try {
// 1. Schicke Daten ans Backend (muss im Backend JSON zurückgeben!)
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.');
}
// 2. Erfolg: Formular verstecken, Ladebildschirm zeigen
formState.style.display = 'none';
loadingState.style.display = 'block';
// 3. 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) {
// Fake-Texte für den Nutzer, damit er weiß, dass das System arbeitet
const statusMessages = [
"Prüfe Serverkapazitäten...",
"Richte Datenbank ein...",
"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);
// Das eigentliche Polling zum Backend
const checkStatus = async () => {
try {
// Backend-Route, die den Status des tenant_slug abfragt
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);
} 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) {
// Bei Netzwerkfehlern während des Pollings einfach weiter probieren
setTimeout(checkStatus, 3000);
}
};
// Starte den ersten Check nach 2 Sekunden
setTimeout(checkStatus, 2000);
}
// ==========================================
// Erfolgs-Bildschirm anzeigen
// ==========================================
function showSuccessState(url, user, pass) {
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;
}
})();
</script>