diff --git a/app/ledger.php b/app/ledger.php index cc447a1..0a0abe3 100644 --- a/app/ledger.php +++ b/app/ledger.php @@ -186,6 +186,17 @@ function ledger_fetch_participant_summaries(PDO $pdo, int $tenantId, array $opti 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 = " SELECT p.id AS participant_id, diff --git a/assets/css/public.css b/assets/css/public.css index 150bcf8..5293522 100644 --- a/assets/css/public.css +++ b/assets/css/public.css @@ -71,11 +71,23 @@ body.public-page { 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 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.25rem; max-width: 72rem; + margin-left: auto; + margin-right: auto; } .public-feature, @@ -84,6 +96,13 @@ body.public-page { border-radius: 8px; padding: 1.5rem; 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 { @@ -94,6 +113,18 @@ body.public-page { 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 { min-height: 100vh; background-image: url("../images/landing-hero.png"); @@ -171,6 +202,8 @@ body.public-page { .public-demo-frame { max-width: 72rem; + margin-left: auto; + margin-right: auto; border: solid 1px rgba(210, 215, 217, 0.75); border-radius: 8px; overflow: hidden; @@ -188,6 +221,8 @@ body.public-page { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.25rem; max-width: 72rem; + margin-left: auto; + margin-right: auto; margin-bottom: 1.5rem; } @@ -221,14 +256,8 @@ body.public-page { margin: 0 0.5rem; } -.public-cta { - text-align: center; -} - -.public-cta .major { - display: flex; - flex-direction: column; - align-items: center; +.public-cta ul.actions { + justify-content: center; } @media screen and (max-width: 980px) { diff --git a/index.php b/index.php index 969ea25..beda22a 100644 --- a/index.php +++ b/index.php @@ -38,11 +38,25 @@ if (!$hasAccess && checkKaffeelisteAccess($conn, $mailadress)) { } $participant = null; -if ($hasAccess && $emailNorm !== '') { - $summaries = ledger_fetch_participant_summaries($pdo, $tenantId, [ - 'email_norms' => [$emailNorm], - ]); - $participant = $summaries[0] ?? null; +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, [ + 'email_norms' => [$emailNorm], + ]); + $participant = $summaries[0] ?? 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} */ -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) { return ['ok' => false, 'error' => 'Die eigene Stricherfassung ist aktuell deaktiviert.']; } @@ -109,25 +116,48 @@ function dashboard_record_legacy_self_entry(PDO $pdo, array $participant, array return ['ok' => false, 'error' => 'Der Preis pro Strich ist nicht gültig konfiguriert.']; } - $bookedAt = date('Y-m-d H:i:s'); - $cost = (($marks * $unitPriceCents) / 100); - $unitPrice = ($unitPriceCents / 100); + $legacyMitarbeiterId = $participant['legacy_mitarbeiter_id'] !== null + ? (int)$participant['legacy_mitarbeiter_id'] + : 0; try { - $pdo->beginTransaction(); - $stmt = $pdo->prepare( - 'INSERT INTO kl_Kaffeeverbrauch - (MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum, Eintragsart) - VALUES (?, ?, ?, ?, ?, 2)' - ); - $stmt->execute([$legacyMitarbeiterId, $marks, $cost, $unitPrice, $bookedAt]); - $legacyConsumptionId = (int)$pdo->lastInsertId(); - if ($legacyConsumptionId <= 0) { - throw new RuntimeException('Legacy-Strich konnte nicht angelegt werden.'); - } + 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'); + $cost = (($marks * $unitPriceCents) / 100); + $unitPrice = ($unitPriceCents / 100); - ledger_mirror_legacy_consumption($pdo, (int)$participant['tenant_id'], $legacyConsumptionId); - $pdo->commit(); + $pdo->beginTransaction(); + $stmt = $pdo->prepare( + 'INSERT INTO kl_Kaffeeverbrauch + (MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum, Eintragsart) + VALUES (?, ?, ?, ?, ?, 2)' + ); + $stmt->execute([$legacyMitarbeiterId, $marks, $cost, $unitPrice, $bookedAt]); + $legacyConsumptionId = (int)$pdo->lastInsertId(); + if ($legacyConsumptionId <= 0) { + throw new RuntimeException('Legacy-Strich konnte nicht angelegt werden.'); + } + + ledger_mirror_legacy_consumption($pdo, (int)$participant['tenant_id'], $legacyConsumptionId); + $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) { if ($pdo->inTransaction()) { $pdo->rollBack(); @@ -174,7 +204,7 @@ function dashboard_render_consumption(array $entries): void if ($_SERVER["REQUEST_METHOD"] === "POST") { 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']) { $entryMessage = $result['message'] ?? null; $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']; $balance = dashboard_money($balanceCents); $currentName = dashboard_current_name($saasUser, $participant, $conn, $mailadress); - $selfEntryEnabled = (int)$settings['self_entry_enabled'] === 1 - && $participant['legacy_mitarbeiter_id'] !== null; + $selfEntryEnabled = (int)$settings['self_entry_enabled'] === 1; $paypalEnabled = (int)$settings['paypal_enabled'] === 1 && trim((string)$settings['paypal_url_template']) !== ''; ?> diff --git a/landing.php b/landing.php index 9393e7d..e8d58c2 100644 --- a/landing.php +++ b/landing.php @@ -5,7 +5,7 @@ Kaffeeliste – die digitale Kaffeekasse für Teams - + @@ -21,7 +21,7 @@

Die Kaffeekasse, die von allein mitrechnet

-

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.

+

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.

+
+

Was kostet die Kaffeeliste?

+

Bis 10 Teilnehmer kostenlos, danach gestaffelt nach Teamgröße ab 3,99 € im Monat – ohne Kosten pro Kopf. Details unter Preise.

+

Sind unsere Daten von denen anderer Kunden getrennt?

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.