Lizenzmodell: Plan-Anzeige in Einstellungen, SEPA-Gate für Free-Plan

This commit is contained in:
rene 2026-05-20 20:21:21 +02:00
parent b8e2a69912
commit 95c2dc0f26
2 changed files with 119 additions and 7 deletions

View file

@ -20,6 +20,11 @@
let saving = $state(false); let saving = $state(false);
let formError = $state(''); let formError = $state('');
// Plan-Check
const hatSepa = $derived(
verein?.plan && ['starter', 'wachstum', 'verband'].includes(verein.plan)
);
// --- SEPA-Export --- // --- SEPA-Export ---
let sepaFor = $state<Beitrag | null>(null); let sepaFor = $state<Beitrag | null>(null);
let einzugsdatum = $state(minEinzugsdatum()); let einzugsdatum = $state(minEinzugsdatum());
@ -189,9 +194,14 @@
<button class="btn-primary" onclick={neuerBeitrag}>+ Beitragsart</button> <button class="btn-primary" onclick={neuerBeitrag}>+ Beitragsart</button>
</div> </div>
{#if sepaFehlt && !loading} {#if !loading && !hatSepa}
<div class="plan-hinweis">
<strong>SEPA-Export</strong> ist im <strong>Starter-Plan</strong> verfügbar (7 €/Monat).
<a href="/einstellungen">Jetzt upgraden →</a>
</div>
{:else if sepaFehlt && !loading}
<div class="hinweis"> <div class="hinweis">
SEPA-Einzug nicht möglich: Gläubiger-ID, IBAN und BIC des Vereins fehlen noch in den Einstellungen. SEPA-Einzug nicht möglich: Gläubiger-ID, IBAN und BIC des Vereins fehlen noch in den <a href="/einstellungen">Einstellungen</a>.
</div> </div>
{/if} {/if}
@ -214,7 +224,7 @@
</span> </span>
</div> </div>
<div class="karte-aktionen"> <div class="karte-aktionen">
<button class="btn-sepa" onclick={() => sepaOeffnen(b)} disabled={sepaFehlt}> <button class="btn-sepa" onclick={() => sepaOeffnen(b)} disabled={sepaFehlt || !hatSepa}>
SEPA SEPA
</button> </button>
<button class="btn-icon" onclick={() => bearbeiten(b)} title="Bearbeiten"></button> <button class="btn-icon" onclick={() => bearbeiten(b)} title="Bearbeiten"></button>
@ -344,6 +354,12 @@
} }
h1 { font-size: 1.4rem; font-weight: 700; color: #1e293b; } h1 { font-size: 1.4rem; font-weight: 700; color: #1e293b; }
.plan-hinweis {
background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px;
padding: 0.75rem 1rem; font-size: 0.875rem; color: #1e40af; margin-bottom: 1rem;
}
.plan-hinweis a { color: #1e40af; font-weight: 700; }
.hinweis { .hinweis {
background: #fef9c3; background: #fef9c3;
border: 1px solid #fde047; border: 1px solid #fde047;
@ -353,6 +369,7 @@
color: #713f12; color: #713f12;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.hinweis a { color: #713f12; }
.hint { color: #94a3b8; font-size: 0.95rem; text-align: center; margin-top: 3rem; } .hint { color: #94a3b8; font-size: 0.95rem; text-align: center; margin-top: 3rem; }

View file

@ -26,6 +26,10 @@
let iban = $state(''); let iban = $state('');
let bic = $state(''); let bic = $state('');
// Plan
let plan = $state<Verein['plan']>('free');
let mitgliederAnz = $state(0);
// Trainer // Trainer
let trainer = $state<any[]>([]); let trainer = $state<any[]>([]);
let gruppen = $state<Gruppe[]>([]); let gruppen = $state<Gruppe[]>([]);
@ -34,6 +38,17 @@
let vereinId = $state(''); let vereinId = $state('');
const planInfo: Record<string, { label: string; farbe: string; features: string[]; limit: number | null }> = {
free: { label: 'Kostenlos', farbe: '#64748b', limit: 50, features: ['Bis 50 Mitglieder', 'Termine & Wiederholungen', 'Nachrichten & Push', 'Veranstaltungsorte', 'Durchführende einladen'] },
starter: { label: 'Starter', farbe: '#1e40af', limit: 150, features: ['Bis 150 Mitglieder', 'SEPA pain.008-Export', 'iCal-Kalender-Abo', 'Alle Free-Features'] },
wachstum:{ label: 'Verband', farbe: '#7c3aed', limit: null, features: ['Unbegrenzte Mitglieder', 'Mehrere Admins', 'Prioritäts-Support', 'Alle Starter-Features'] },
verband: { label: 'Verband', farbe: '#7c3aed', limit: null, features: ['Unbegrenzte Mitglieder', 'Mehrere Admins', 'Prioritäts-Support', 'Alle Starter-Features'] },
};
const istFree = $derived(plan === 'free');
const istStarter = $derived(plan === 'starter' || plan === 'wachstum' || plan === 'verband');
const limitErreicht = $derived(istFree && mitgliederAnz > 50);
const bundeslaender = [ const bundeslaender = [
['', '—'], ['', '—'],
['BW', 'Baden-Württemberg'], ['BY', 'Bayern'], ['BE', 'Berlin'], ['BW', 'Baden-Württemberg'], ['BY', 'Bayern'], ['BE', 'Berlin'],
@ -46,15 +61,18 @@
onMount(async () => { onMount(async () => {
vereinId = pb.authStore.record?.verein_id as string; vereinId = pb.authStore.record?.verein_id as string;
const [v, alleUser, alleGruppen] = await Promise.all([ const [v, alleUser, alleGruppen, mitgliederCount] = await Promise.all([
pb.collection('vereine').getOne<Verein>(vereinId), pb.collection('vereine').getOne<Verein>(vereinId),
isAdmin() isAdmin()
? pb.collection('users').getFullList({ filter: `verein_id = "${vereinId}"` }) ? pb.collection('users').getFullList({ filter: `verein_id = "${vereinId}"` })
: Promise.resolve([]), : Promise.resolve([]),
pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }), pb.collection('gruppen').getFullList<Gruppe>({ sort: 'name' }),
pb.collection('mitglieder').getList(1, 1, { filter: `verein_id = "${vereinId}"` }).then(r => r.totalItems),
]); ]);
trainer = alleUser.filter((u: any) => u.rolle === 'trainer'); trainer = alleUser.filter((u: any) => u.rolle === 'trainer');
gruppen = alleGruppen; gruppen = alleGruppen;
plan = v.plan ?? 'free';
mitgliederAnz = mitgliederCount;
name = v.name ?? ''; name = v.name ?? '';
adresse = v.adresse ?? ''; adresse = v.adresse ?? '';
plz = v.plz ?? ''; plz = v.plz ?? '';
@ -129,6 +147,47 @@
{#if loading} {#if loading}
<p class="hint">Laden…</p> <p class="hint">Laden…</p>
{:else} {:else}
<!-- Plan-Übersicht -->
{@const pi = planInfo[plan] ?? planInfo.free}
<div class="plan-card">
<div class="plan-header">
<div>
<span class="plan-label" style="color:{pi.farbe}">{pi.label}</span>
{#if pi.limit}
<span class="plan-limit">bis {pi.limit} Mitglieder</span>
{:else}
<span class="plan-limit">unbegrenzte Mitglieder</span>
{/if}
</div>
<span class="plan-mitglieder" class:warn={limitErreicht}>
{mitgliederAnz} Mitglieder
</span>
</div>
{#if limitErreicht}
<div class="plan-warn">
⚠ Du hast das Limit von 50 Mitgliedern überschritten. Neue Mitglieder können nicht angelegt werden.
</div>
{/if}
<ul class="plan-features">
{#each pi.features as f}
<li>{f}</li>
{/each}
{#if istFree}
<li class="gesperrt">✗ SEPA-Export <span class="upgrade-hint">Starter</span></li>
<li class="gesperrt">✗ iCal-Kalender-Abo <span class="upgrade-hint">Starter</span></li>
{/if}
</ul>
{#if istFree}
<a href="mailto:info@vereins.haus?subject=Upgrade%20auf%20Starter&body=Vereins-ID%3A%20{vereinId}" class="btn-upgrade">
Auf Starter upgraden 7 €/Monat →
</a>
{/if}
</div>
<form onsubmit={(e) => { e.preventDefault(); speichern(); }}> <form onsubmit={(e) => { e.preventDefault(); speichern(); }}>
<section> <section>
@ -275,7 +334,43 @@
{/if} {/if}
<style> <style>
h1 { font-size: 1.4rem; font-weight: 700; color: #1e293b; margin-bottom: 1.5rem; } h1 { font-size: 1.4rem; font-weight: 700; color: #1e293b; margin-bottom: 1rem; }
/* Plan */
.plan-card {
background: #fff; border: 1.5px solid #e2e8f0; border-radius: 12px;
padding: 1rem 1.1rem; margin-bottom: 1.5rem;
}
.plan-header {
display: flex; justify-content: space-between; align-items: baseline;
margin-bottom: 0.6rem;
}
.plan-label { font-size: 1rem; font-weight: 700; }
.plan-limit { font-size: 0.78rem; color: #94a3b8; margin-left: 0.4rem; }
.plan-mitglieder { font-size: 0.82rem; color: #64748b; }
.plan-mitglieder.warn { color: #dc2626; font-weight: 700; }
.plan-warn {
background: #fef2f2; border: 1px solid #fca5a5; border-radius: 8px;
padding: 0.6rem 0.8rem; font-size: 0.82rem; color: #dc2626; margin-bottom: 0.75rem;
}
.plan-features {
list-style: none; padding: 0; margin: 0 0 0.85rem;
display: flex; flex-direction: column; gap: 0.25rem;
}
.plan-features li { font-size: 0.85rem; color: #475569; }
.plan-features li.gesperrt { color: #94a3b8; }
.upgrade-hint {
display: inline-block; font-size: 0.7rem; font-weight: 700;
background: #e0e7ff; color: #1e40af; border-radius: 4px;
padding: 0.05rem 0.35rem; margin-left: 0.3rem;
}
.btn-upgrade {
display: block; width: 100%; padding: 0.65rem;
background: #1e40af; color: #fff; border: none; border-radius: 8px;
font-size: 0.9rem; font-weight: 600; text-align: center;
text-decoration: none; transition: background 0.15s;
}
.btn-upgrade:hover { background: #1d3a9e; }
section { section {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;