Überarbeitung landing und index

This commit is contained in:
2026-07-20 19:36:22 +02:00
parent c9ab82e97a
commit 9871401fbc
4 changed files with 194 additions and 58 deletions
+11
View File
@@ -186,6 +186,17 @@ function ledger_fetch_participant_summaries(PDO $pdo, int $tenantId, array $opti
array_push($params, ...$emailNorms); array_push($params, ...$emailNorms);
} }
if (array_key_exists('user_ids', $options)) {
$userIds = is_array($options['user_ids'])
? ledger_normalize_ids($options['user_ids'])
: [];
if ($userIds === []) {
return [];
}
$where[] = 'p.user_id IN (' . implode(',', array_fill(0, count($userIds), '?')) . ')';
array_push($params, ...$userIds);
}
$sql = " $sql = "
SELECT SELECT
p.id AS participant_id, p.id AS participant_id,
+37 -8
View File
@@ -71,11 +71,23 @@ body.public-page {
padding: 4rem 6vw; padding: 4rem 6vw;
} }
/* Section intros (Titel + Unterzeile) sind ueberall zentriert, damit die
Seite nicht zwischen linksbuendigen und zentrierten Abschnitten springt.
Die Karteninhalte darunter bleiben davon unberuehrt links ausgerichtet. */
.public-section > header.major {
text-align: center;
max-width: 42rem;
margin-left: auto;
margin-right: auto;
}
.public-grid { .public-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1.25rem; gap: 1.25rem;
max-width: 72rem; max-width: 72rem;
margin-left: auto;
margin-right: auto;
} }
.public-feature, .public-feature,
@@ -84,6 +96,13 @@ body.public-page {
border-radius: 8px; border-radius: 8px;
padding: 1.5rem; padding: 1.5rem;
background: #ffffff; background: #ffffff;
text-align: left;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.public-feature:hover {
transform: translateY(-0.2rem);
box-shadow: 0 0.75rem 1.75rem rgba(61, 68, 73, 0.1);
} }
.public-feature h3 { .public-feature h3 {
@@ -94,6 +113,18 @@ body.public-page {
margin-bottom: 0; margin-bottom: 0;
} }
.public-highlight {
background: #eef4ea;
}
.public-fineprint {
max-width: 72rem;
margin-left: auto;
margin-right: auto;
color: #7f888f;
font-size: 0.9em;
}
.public-auth { .public-auth {
min-height: 100vh; min-height: 100vh;
background-image: url("../images/landing-hero.png"); background-image: url("../images/landing-hero.png");
@@ -171,6 +202,8 @@ body.public-page {
.public-demo-frame { .public-demo-frame {
max-width: 72rem; max-width: 72rem;
margin-left: auto;
margin-right: auto;
border: solid 1px rgba(210, 215, 217, 0.75); border: solid 1px rgba(210, 215, 217, 0.75);
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
@@ -188,6 +221,8 @@ body.public-page {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1.25rem; gap: 1.25rem;
max-width: 72rem; max-width: 72rem;
margin-left: auto;
margin-right: auto;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
@@ -221,14 +256,8 @@ body.public-page {
margin: 0 0.5rem; margin: 0 0.5rem;
} }
.public-cta { .public-cta ul.actions {
text-align: center; justify-content: center;
}
.public-cta .major {
display: flex;
flex-direction: column;
align-items: center;
} }
@media screen and (max-width: 980px) { @media screen and (max-width: 980px) {
+42 -13
View File
@@ -38,11 +38,25 @@ if (!$hasAccess && checkKaffeelisteAccess($conn, $mailadress)) {
} }
$participant = null; $participant = null;
if ($hasAccess && $emailNorm !== '') { if ($hasAccess) {
// Eingeloggte SaaS-Nutzer werden primaer ueber die feste user_id-
// Verknuepfung ihres Teilnehmers gefunden (bei Registrierung bzw.
// Zugangsvergabe gesetzt) - robuster als der E-Mail-Abgleich, der bei
// abweichender oder geleerter Teilnehmer-E-Mail ins Leere liefe. Der
// Legacy-/Dev-Pfad ohne SaaS-User faellt weiterhin auf die E-Mail zurueck.
$saasUserId = $saasUser !== null ? (int)$saasUser['user_id'] : 0;
if ($saasUserId > 0) {
$summaries = ledger_fetch_participant_summaries($pdo, $tenantId, [
'user_ids' => [$saasUserId],
]);
$participant = $summaries[0] ?? null;
}
if ($participant === null && $emailNorm !== '') {
$summaries = ledger_fetch_participant_summaries($pdo, $tenantId, [ $summaries = ledger_fetch_participant_summaries($pdo, $tenantId, [
'email_norms' => [$emailNorm], 'email_norms' => [$emailNorm],
]); ]);
$participant = $summaries[0] ?? null; $participant = $summaries[0] ?? null;
}
} }
$settings = $hasAccess ? saas_fetch_tenant_settings($pdo, $tenantId) : null; $settings = $hasAccess ? saas_fetch_tenant_settings($pdo, $tenantId) : null;
@@ -86,15 +100,8 @@ function dashboard_paypal_action(string $template, string $amount): string
/** /**
* @return array{ok: bool, message?: string, error?: string} * @return array{ok: bool, message?: string, error?: string}
*/ */
function dashboard_record_legacy_self_entry(PDO $pdo, array $participant, array $settings, mixed $marksValue): array function dashboard_record_self_entry(PDO $pdo, array $participant, array $settings, mixed $marksValue, ?int $actorUserId): array
{ {
$legacyMitarbeiterId = $participant['legacy_mitarbeiter_id'] !== null
? (int)$participant['legacy_mitarbeiter_id']
: 0;
if ($legacyMitarbeiterId <= 0) {
return ['ok' => false, 'error' => 'Für diesen Teilnehmer ist die eigene Stricherfassung noch nicht verfügbar.'];
}
if ((int)$settings['self_entry_enabled'] !== 1) { if ((int)$settings['self_entry_enabled'] !== 1) {
return ['ok' => false, 'error' => 'Die eigene Stricherfassung ist aktuell deaktiviert.']; return ['ok' => false, 'error' => 'Die eigene Stricherfassung ist aktuell deaktiviert.'];
} }
@@ -109,11 +116,20 @@ function dashboard_record_legacy_self_entry(PDO $pdo, array $participant, array
return ['ok' => false, 'error' => 'Der Preis pro Strich ist nicht gültig konfiguriert.']; return ['ok' => false, 'error' => 'Der Preis pro Strich ist nicht gültig konfiguriert.'];
} }
$legacyMitarbeiterId = $participant['legacy_mitarbeiter_id'] !== null
? (int)$participant['legacy_mitarbeiter_id']
: 0;
try {
if ($legacyMitarbeiterId > 0) {
// Migrierter Default-Mandant: weiterhin in die Legacy-Tabelle
// schreiben, damit die noch-legacy Sammelseiten (stricheintragen.php)
// den Eintrag ebenfalls sehen; der Ledger-Eintrag entsteht per
// Spiegelung.
$bookedAt = date('Y-m-d H:i:s'); $bookedAt = date('Y-m-d H:i:s');
$cost = (($marks * $unitPriceCents) / 100); $cost = (($marks * $unitPriceCents) / 100);
$unitPrice = ($unitPriceCents / 100); $unitPrice = ($unitPriceCents / 100);
try {
$pdo->beginTransaction(); $pdo->beginTransaction();
$stmt = $pdo->prepare( $stmt = $pdo->prepare(
'INSERT INTO kl_Kaffeeverbrauch 'INSERT INTO kl_Kaffeeverbrauch
@@ -128,6 +144,20 @@ function dashboard_record_legacy_self_entry(PDO $pdo, array $participant, array
ledger_mirror_legacy_consumption($pdo, (int)$participant['tenant_id'], $legacyConsumptionId); ledger_mirror_legacy_consumption($pdo, (int)$participant['tenant_id'], $legacyConsumptionId);
$pdo->commit(); $pdo->commit();
} else {
// SaaS-native Mandanten (alle ausser dem migrierten Default-
// Mandanten) haben keine Legacy-Schattentabelle - direkt ins
// Ledger buchen.
ledger_record_consumption(
$pdo,
(int)$participant['tenant_id'],
(int)$participant['participant_id'],
$marks,
$unitPriceCents,
'self_entry',
$actorUserId
);
}
} catch (Throwable $e) { } catch (Throwable $e) {
if ($pdo->inTransaction()) { if ($pdo->inTransaction()) {
$pdo->rollBack(); $pdo->rollBack();
@@ -174,7 +204,7 @@ function dashboard_render_consumption(array $entries): void
if ($_SERVER["REQUEST_METHOD"] === "POST") { if ($_SERVER["REQUEST_METHOD"] === "POST") {
if ($hasAccess && $participant !== null && $settings !== null) { if ($hasAccess && $participant !== null && $settings !== null) {
$result = dashboard_record_legacy_self_entry($pdo, $participant, $settings, $_POST["anzahlStriche"] ?? null); $result = dashboard_record_self_entry($pdo, $participant, $settings, $_POST["anzahlStriche"] ?? null, $saasUser['user_id'] ?? null);
if ($result['ok']) { if ($result['ok']) {
$entryMessage = $result['message'] ?? null; $entryMessage = $result['message'] ?? null;
$participant = ledger_fetch_participant_summary($pdo, $tenantId, (int)$participant['participant_id']); $participant = ledger_fetch_participant_summary($pdo, $tenantId, (int)$participant['participant_id']);
@@ -213,8 +243,7 @@ if ($hasAccess && $participant !== null && $settings !== null) {
$balanceCents = (int)$participant['balance_cents']; $balanceCents = (int)$participant['balance_cents'];
$balance = dashboard_money($balanceCents); $balance = dashboard_money($balanceCents);
$currentName = dashboard_current_name($saasUser, $participant, $conn, $mailadress); $currentName = dashboard_current_name($saasUser, $participant, $conn, $mailadress);
$selfEntryEnabled = (int)$settings['self_entry_enabled'] === 1 $selfEntryEnabled = (int)$settings['self_entry_enabled'] === 1;
&& $participant['legacy_mitarbeiter_id'] !== null;
$paypalEnabled = (int)$settings['paypal_enabled'] === 1 $paypalEnabled = (int)$settings['paypal_enabled'] === 1
&& trim((string)$settings['paypal_url_template']) !== ''; && trim((string)$settings['paypal_url_template']) !== '';
?> ?>
+85 -18
View File
@@ -5,7 +5,7 @@
<title>Kaffeeliste die digitale Kaffeekasse für Teams</title> <title>Kaffeeliste die digitale Kaffeekasse für Teams</title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<meta name="description" content="Kaffeeliste ist die digitale Kaffeekasse für Teams, Büros und Vereine: Striche erfassen, Guthaben im Blick behalten, bequem per PayPal ausgleichen." /> <meta name="description" content="Kaffeeliste automatisiert die Abrechnung eurer Kaffeekasse: Guthaben, Zahlungen und offene Beträge immer im Blick ab 0 € im Monat, in Minuten startklar." />
<link rel="stylesheet" href="assets/css/main.css" /> <link rel="stylesheet" href="assets/css/main.css" />
<link rel="stylesheet" href="assets/css/public.css" /> <link rel="stylesheet" href="assets/css/public.css" />
</head> </head>
@@ -21,7 +21,7 @@
</nav> </nav>
<div class="public-hero-content"> <div class="public-hero-content">
<h1>Die Kaffeekasse, die von allein mitrechnet</h1> <h1>Die Kaffeekasse, die von allein mitrechnet</h1>
<p>Schluss mit Strichlisten auf Papier und Kopfrechnen beim Kassensturz: Kaffeeliste hält für dein Team, Büro oder deinen Verein fest, wer wie viele Striche gemacht hat, wer bezahlt hat und wer gerade offen ist jederzeit einsehbar, für jeden nachvollziehbar.</p> <p>Die Striche macht ihr weiterhin ganz klassisch an der Maschine auf der ausgedruckten Liste, wie eh und je. Den Rest übernimmt Kaffeeliste: Guthaben, Zahlungen und offene Beträge sind für alle jederzeit auf einen Blick sichtbar. Schluss mit Kopfrechnen, Excel-Tabellen und dem lästigen Kassensturz am Monatsende.</p>
<ul class="actions"> <ul class="actions">
<li><a href="<?php echo htmlspecialchars(app_primary_url('register.php'), ENT_QUOTES, 'UTF-8'); ?>" class="button primary big">Kostenlos einrichten</a></li> <li><a href="<?php echo htmlspecialchars(app_primary_url('register.php'), ENT_QUOTES, 'UTF-8'); ?>" class="button primary big">Kostenlos einrichten</a></li>
<li><a href="<?php echo htmlspecialchars(app_primary_url('login.php'), ENT_QUOTES, 'UTF-8'); ?>" class="button big">Zur App</a></li> <li><a href="<?php echo htmlspecialchars(app_primary_url('login.php'), ENT_QUOTES, 'UTF-8'); ?>" class="button big">Zur App</a></li>
@@ -32,6 +32,7 @@
<section class="public-section"> <section class="public-section">
<header class="major"> <header class="major">
<h2>So funktioniert's</h2> <h2>So funktioniert's</h2>
<p>Drei Schritte, kein Umlernen für dein Team nötig.</p>
</header> </header>
<div class="public-grid public-steps"> <div class="public-grid public-steps">
<article class="public-feature"> <article class="public-feature">
@@ -42,12 +43,12 @@
<article class="public-feature"> <article class="public-feature">
<span class="public-step-number">2</span> <span class="public-step-number">2</span>
<h3>Strich setzen</h3> <h3>Strich setzen</h3>
<p>Der Strich landet in der Kaffeeliste entweder durch eine verantwortliche Person gesammelt oder, wenn gewünscht, direkt selbst im eigenen Dashboard.</p> <p>Der Strich landet in der Kaffeeliste gesammelt durch die zuständige Person oder, wenn gewünscht, in Sekunden selbst im eigenen Dashboard erfasst.</p>
</article> </article>
<article class="public-feature"> <article class="public-feature">
<span class="public-step-number">3</span> <span class="public-step-number">3</span>
<h3>Bei Bedarf bezahlen</h3> <h3>Bei Bedarf bezahlen</h3>
<p>Der aktuelle Stand ist jederzeit sichtbar. Ausgleich bequem per PayPal, wenn dein Team das nutzt ganz ohne Bargeld und Wechselgeld.</p> <p>Der aktuelle Stand ist immer sichtbar. Ausgleich in einem Klick per PayPal, ganz ohne Bargeld und Wechselgeld-Suche.</p>
</article> </article>
</div> </div>
</section> </section>
@@ -55,35 +56,97 @@
<section class="public-section"> <section class="public-section">
<header class="major"> <header class="major">
<h2>Alles rund um die Kaffeekasse</h2> <h2>Alles rund um die Kaffeekasse</h2>
<p>Kein Feature-Wildwuchs nur, was ein Team wirklich braucht.</p>
</header> </header>
<div class="public-grid"> <div class="public-grid">
<article class="public-feature"> <article class="public-feature">
<h3>Mitglieder &amp; Rollen</h3> <h3>Immer der richtige Zugriff</h3>
<p>Mitglieder anlegen, deaktivieren oder anonymisieren. Zugang mit passender Rolle (Admin, Kassenwart, Mitglied) gezielt vergeben oder wieder entziehen.</p> <p>Neue Kolleg:innen sind in einer Minute angelegt, wer geht, ist genauso schnell wieder draußen. Rollen wie Admin oder Kassenwart vergebt ihr gezielt zum Beispiel für die Praktikantin, die nur Striche einträgt, aber nichts an den Einstellungen ändern kann.</p>
</article> </article>
<article class="public-feature"> <article class="public-feature">
<h3>Guthaben &amp; Einzahlungen</h3> <h3>Nie wieder „Wer hat schon bezahlt?"</h3>
<p>Striche, Einzahlungen und Korrekturen laufen über ein durchgängiges Journal jede Buchung bleibt nachvollziehbar, auch nachträglich.</p> <p>Jede Einzahlung, jeder Strich, jede Korrektur landet automatisch im gemeinsamen Kontobuch nachvollziehbar bis zum ersten Tag. Praktisch beim Jahresabschluss: ein Klick, fertig.</p>
</article> </article>
<article class="public-feature"> <article class="public-feature">
<h3>PayPal-Zahlung</h3> <h3>Bezahlen in 10 Sekunden</h3>
<p>Offene Beträge lassen sich optional direkt per PayPal ausgleichen, inklusive automatischem Abgleich in der Liste.</p> <p>Wer offen ist, bekommt einen fertigen PayPal-Link mit passendem Betrag. Kein Bargeld, kein Wechselgeld-Chaos in der Teeküche.</p>
</article> </article>
<article class="public-feature"> <article class="public-feature">
<h3>CSV-Import &amp; Export</h3> <h3>Die Liste an der Wand bleibt</h3>
<p>Zahlungen aus einem PayPal-Export einlesen, mit Vorschau und Dublettenprüfung vor der Übernahme. Kaffeeliste als PDF für den Ausdruck exportieren.</p> <p>Ausdrucken, aufhängen, fertig mit eigenem Hinweistext als Wasserzeichen und automatisch passender Zeilengröße, egal ob 5 oder 150 Kolleg:innen mitmachen.</p>
</article> </article>
<article class="public-feature"> <article class="public-feature">
<h3>Mandantenfähig</h3> <h3>Alle informiert, ohne Rundmail-Chaos</h3>
<p>Jeder Kunde arbeitet in einem eigenen, streng getrennten Bereich mit eigenen Mitgliedern, Einstellungen und Buchungen.</p> <p>Ein Hinweisbanner für „Die Maschine ist kaputt" oder eine Erinnerungsmail an alle mit offenem Betrag in einer Minute verschickt statt einzeln nachgefragt.</p>
</article>
<article class="public-feature">
<h3>Hinweise &amp; Info-Mails</h3>
<p>Aktuelle Hinweise für alle sichtbar einblenden, und bei Bedarf eine Info-Mail mit dem aktuellen Stand an alle Mitglieder verschicken.</p>
</article> </article>
</div> </div>
</section> </section>
<section class="public-section public-highlight">
<header class="major">
<h2>6 Gründe, warum Verwalter Kaffeeliste lieben</h2>
<p>Für Admin und Kassenwart macht das den größten Unterschied im Alltag.</p>
</header>
<div class="public-grid public-steps public-advantages">
<article class="public-feature">
<span class="public-step-number">1</span>
<h3>Kein Taschenrechner mehr</h3>
<p>Salden berechnen sich automatisch. Keine manuelle Addition von Strichen und Zahlungen am Monatsende.</p>
</article>
<article class="public-feature">
<span class="public-step-number">2</span>
<h3>Ein Klick statt Excel-Akrobatik</h3>
<p>Jahresabschluss, Mitgliederliste, Export direkt aus der App statt mühsamer Tabellenpflege nebenbei.</p>
</article>
<article class="public-feature">
<span class="public-step-number">3</span>
<h3>Erinnerungen laufen von allein</h3>
<p>Eine Info-Mail an alle mit offenem Betrag, statt jede Person einzeln anzuschreiben oder anzusprechen.</p>
</article>
<article class="public-feature">
<span class="public-step-number">4</span>
<h3>Mitgliederverwaltung in Sekunden</h3>
<p>Neue Kolleg:innen anlegen, alte deaktivieren, Rollen vergeben ohne Rückfragen, wer worauf Zugriff hat.</p>
</article>
<article class="public-feature">
<span class="public-step-number">5</span>
<h3>PayPal-Abgleich ohne Nachrechnen</h3>
<p>Eingehende Zahlungen ordnen sich automatisch dem richtigen Mitglied zu kein Abgleich von Hand mehr.</p>
</article>
<article class="public-feature">
<span class="public-step-number">6</span>
<h3>Der Stand ist immer griffbereit</h3>
<p>Ob am Handy in der Teeküche oder am Rechner im Büro: Der Kassenstand ist nie veraltet oder in der falschen Datei.</p>
</article>
</div>
</section>
<section class="public-section">
<header class="major">
<h2>Preise</h2>
<p>Ein Preis pro Team, gestaffelt nach aktiven Teilnehmern keine Kosten pro Kopf, keine versteckten Gebühren.</p>
</header>
<div class="public-grid">
<article class="public-feature">
<h3>Bis 10 Teilnehmer</h3>
<p><strong>Kostenlos</strong></p>
</article>
<article class="public-feature">
<h3>Bis 25 Teilnehmer</h3>
<p><strong>3,99 € / Monat</strong></p>
</article>
<article class="public-feature">
<h3>Bis 50 Teilnehmer</h3>
<p><strong>7,99 € / Monat</strong></p>
</article>
<article class="public-feature">
<h3>Bis 150 Teilnehmer</h3>
<p><strong>12,99 € / Monat</strong></p>
</article>
</div>
<p class="public-fineprint">Mehr als 150 Teilnehmer? <a href="<?php echo htmlspecialchars(app_primary_url('preise.php'), ENT_QUOTES, 'UTF-8'); ?>">Preis auf Anfrage</a>. Gemäß § 19 UStG wird keine Umsatzsteuer berechnet die genannten Beträge sind die tatsächlich zu zahlenden Endpreise. Details unter <a href="<?php echo htmlspecialchars(app_primary_url('preise.php'), ENT_QUOTES, 'UTF-8'); ?>">Preise</a>.</p>
</section>
<section class="public-section public-demo"> <section class="public-section public-demo">
<header class="major"> <header class="major">
<h2>So sieht's in der App aus</h2> <h2>So sieht's in der App aus</h2>
@@ -103,6 +166,10 @@
<h3>Wie schnell ist meine Kaffeeliste startklar?</h3> <h3>Wie schnell ist meine Kaffeeliste startklar?</h3>
<p>Kundenkonto anlegen, Mitglieder eintragen, loslegen. Es ist keine Einrichtung durch uns nötig, und die bestehenden Abläufe rund um Striche und Einzahlungen bleiben dieselben.</p> <p>Kundenkonto anlegen, Mitglieder eintragen, loslegen. Es ist keine Einrichtung durch uns nötig, und die bestehenden Abläufe rund um Striche und Einzahlungen bleiben dieselben.</p>
</div> </div>
<div class="public-panel">
<h3>Was kostet die Kaffeeliste?</h3>
<p>Bis 10 Teilnehmer kostenlos, danach gestaffelt nach Teamgröße ab 3,99 € im Monat ohne Kosten pro Kopf. Details unter <a href="<?php echo htmlspecialchars(app_primary_url('preise.php'), ENT_QUOTES, 'UTF-8'); ?>">Preise</a>.</p>
</div>
<div class="public-panel"> <div class="public-panel">
<h3>Sind unsere Daten von denen anderer Kunden getrennt?</h3> <h3>Sind unsere Daten von denen anderer Kunden getrennt?</h3>
<p>Ja. Jeder Kunde hat einen eigenen, isolierten Bereich für Mitglieder, Buchungen und Einstellungen. Diese Trennung ist fester Bestandteil der App und wird automatisiert geprüft.</p> <p>Ja. Jeder Kunde hat einen eigenen, isolierten Bereich für Mitglieder, Buchungen und Einstellungen. Diese Trennung ist fester Bestandteil der App und wird automatisiert geprüft.</p>