diff --git a/.env.example b/.env.example index 93cbad8..59ff20a 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_NAME="Kaffeekasse SaaS" +APP_NAME="Kaffeeliste" APP_ENV=production APP_DEBUG=0 APP_URL=http://localhost:8080 @@ -17,14 +17,14 @@ DB_PASS= MAIL_FROM=noreply@example.com MAIL_REPLY_TO= -MAIL_SUBJECT_PREFIX="[Kaffeekasse] " +MAIL_SUBJECT_PREFIX="[Kaffeeliste] " MAIL_LOG_FALLBACK=1 MAIL_LOG_FILE= PASSWORD_RESET_URL=http://localhost:8080/reset-password?token={{token}} PASSWORD_RESET_TTL_MINUTES=60 PASSWORD_RESET_MAX_ACTIVE_TOKENS=3 -PASSWORD_RESET_SUBJECT="Passwort zuruecksetzen" +PASSWORD_RESET_SUBJECT="Passwort zurücksetzen" RATE_LIMIT_LOGIN_MAX_ATTEMPTS=5 RATE_LIMIT_LOGIN_WINDOW_SECONDS=900 diff --git a/app/Controllers/PlatformController.php b/app/Controllers/PlatformController.php index 1c872f4..3d8a4d5 100644 --- a/app/Controllers/PlatformController.php +++ b/app/Controllers/PlatformController.php @@ -49,7 +49,7 @@ final class PlatformController } $this->view->render('home/index', [ - 'title' => 'Kaffeekasse fuer Teams, Vereine und Coworking', + 'title' => 'Kaffeeliste für Teams, Vereine und Büroküchen', 'setupComplete' => $setup->isInstalled(), 'stats' => $stats, 'allowPublicRegistration' => $this->config['allow_public_registration'] ?? false, @@ -77,7 +77,7 @@ final class PlatformController try { $setup->install($_POST); - $this->session->flash('success', 'Die Installation wurde abgeschlossen. Jetzt kannst du dich als Plattform-Admin anmelden.'); + $this->session->flash('success', 'Die Installation wurde abgeschlossen. Sie können sich jetzt als Plattform-Administrator anmelden.'); Response::redirect('/admin/login'); } catch (\Throwable $throwable) { remember_old_input($_POST); @@ -93,12 +93,12 @@ final class PlatformController } if (!$this->allowsPublicRegistration()) { - $this->session->flash('error', 'Die offene Registrierung ist derzeit deaktiviert. Fuer einen Pilotzugang bitte direkt den Betreiber kontaktieren.'); + $this->session->flash('error', 'Die offene Registrierung ist derzeit deaktiviert. Bitte kontaktieren Sie den Betreiber.'); Response::redirect('/'); } $this->view->render('home/register', [ - 'title' => 'Mandant registrieren', + 'title' => 'Kaffeeliste registrieren', ]); } @@ -122,7 +122,7 @@ final class PlatformController $service = new TenantRegistrationService($pdo, $audit); $slug = $service->register($_POST); $this->clearScopes($rateLimiter, 'registration', $scopes); - $this->session->flash('success', 'Dein Mandant wurde angelegt. Du kannst dich jetzt anmelden.'); + $this->session->flash('success', 'Die Kaffeeliste wurde angelegt. Sie können sich jetzt anmelden.'); Response::redirect('/t/' . rawurlencode($slug) . '/login'); } catch (\Throwable $throwable) { if ($this->database->isConfigured()) { @@ -141,7 +141,7 @@ final class PlatformController public function adminLoginForm(): void { $this->view->render('admin/login', [ - 'title' => 'Plattform-Login', + 'title' => 'Kaffeelisten verwalten', ]); } @@ -257,7 +257,7 @@ final class PlatformController $passwordConfirm = (string) ($_POST['password_confirm'] ?? ''); if ($password !== $passwordConfirm) { - throw new RuntimeException('Die Passwort-Bestaetigung stimmt nicht ueberein.'); + throw new RuntimeException('Die Passwort-Bestätigung stimmt nicht überein.'); } $pdo = $this->database->pdo(); diff --git a/app/Controllers/TenantController.php b/app/Controllers/TenantController.php index 7bfd919..44b5ebb 100644 --- a/app/Controllers/TenantController.php +++ b/app/Controllers/TenantController.php @@ -33,7 +33,7 @@ final class TenantController $tenant = $this->tenant($tenantSlug); $this->view->render('tenant/login', [ - 'title' => 'Tenant-Login', + 'title' => 'Kaffeelisten-Login', 'tenant' => $tenant, ]); } @@ -67,11 +67,11 @@ final class TenantController if (!$this->isTenantManager($authState) && !$this->selfServiceAllowed((int) $tenant['id'])) { $auth->logout(); - throw new RuntimeException('Die digitale Selbstbuchung ist fuer diesen Mandanten derzeit deaktiviert.'); + throw new RuntimeException('Die Buchung ist für diese Kaffeeliste derzeit deaktiviert.'); } $this->clearRateLimitScopes($rateLimiter, $scopes); - $this->session->flash('success', 'Willkommen zur digitalen Kaffeeliste.'); + $this->session->flash('success', 'Willkommen in Ihrer Kaffeeliste.'); Response::redirect(tenant_url($tenantSlug)); } catch (\Throwable $throwable) { remember_old_input($_POST); @@ -86,7 +86,7 @@ final class TenantController $pdo = $this->database->pdo(); $auth = new Auth($pdo, $this->session); $auth->logout(); - $this->session->flash('success', 'Du wurdest abgemeldet.'); + $this->session->flash('success', 'Sie wurden abgemeldet.'); Response::redirect(tenant_url($tenantSlug, 'login')); } @@ -125,6 +125,7 @@ final class TenantController m.display_name AS member_name, p.name AS product_name, s.name AS source_name, + s.code AS source_code, CASE WHEN reversal.id IS NULL THEN 0 ELSE 1 END AS is_reversed FROM consumption_events c INNER JOIN members m ON m.id = c.member_id @@ -159,7 +160,7 @@ final class TenantController $balancesStatement->execute(['tenant_id' => $tenant['id']]); } else { if ($currentMember === null) { - throw new RuntimeException('Fuer diesen Benutzer ist kein aktives Mitglied im Mandanten hinterlegt.'); + throw new RuntimeException('Für diesen Benutzer ist kein aktives Mitglied in der Kaffeeliste hinterlegt.'); } $summaryStatement = $pdo->prepare( @@ -193,6 +194,7 @@ final class TenantController m.display_name AS member_name, p.name AS product_name, s.name AS source_name, + s.code AS source_code, CASE WHEN reversal.id IS NULL THEN 0 ELSE 1 END AS is_reversed FROM consumption_events c INNER JOIN members m ON m.id = c.member_id @@ -234,7 +236,7 @@ final class TenantController } $this->view->render('tenant/dashboard', [ - 'title' => 'Dashboard', + 'title' => 'Kaffeekasse', 'tenant' => $tenant, 'authState' => $authState, 'summary' => $summary, @@ -271,11 +273,11 @@ final class TenantController if ($email !== '') { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { - throw new RuntimeException('Die E-Mail-Adresse ist ungueltig.'); + throw new RuntimeException('Die E-Mail-Adresse ist ungültig.'); } if ($loginPassword === '' || mb_strlen($loginPassword) < 12) { - throw new RuntimeException('Fuer Login-faehige Mitglieder ist ein Passwort mit mindestens 12 Zeichen noetig.'); + throw new RuntimeException('Für Mitglieder mit Login ist ein Passwort mit mindestens 12 Zeichen nötig.'); } $existingUserStatement = $pdo->prepare('SELECT id FROM users WHERE email = :email LIMIT 1'); @@ -319,7 +321,7 @@ final class TenantController ]); (new AuditService($pdo))->log((int) $tenant['id'], (int) $authState['user_id'], 'member.created', 'member', (int) $pdo->lastInsertId(), 'success'); - $this->session->flash('success', 'Das Mitglied wurde gespeichert.'); + $this->session->flash('success', 'Das Mitglied wurde angelegt.'); } catch (\Throwable $throwable) { remember_old_input($_POST); $this->session->flash('error', $throwable->getMessage()); @@ -406,7 +408,7 @@ final class TenantController $productsStatement->execute(['tenant_id' => $tenant['id']]); $this->view->render('tenant/products', [ - 'title' => 'Produkte', + 'title' => 'Produkte und Preise', 'tenant' => $tenant, 'authState' => $authState, 'products' => $productsStatement->fetchAll(), @@ -429,7 +431,7 @@ final class TenantController if ($action === 'reverse_booking') { if (!$isManager) { - throw new RuntimeException('Nur Admins duerfen Buchungen stornieren.'); + throw new RuntimeException('Nur Administratorinnen und Administratoren dürfen Buchungen stornieren.'); } $service->reverseLedgerEntry( @@ -453,7 +455,7 @@ final class TenantController (float) ($_POST['quantity'] ?? 1), trim((string) ($_POST['notes'] ?? '')) ); - $this->session->flash('success', 'Die Buchung wurde gespeichert.'); + $this->session->flash('success', 'Die Kaffeebuchung wurde gespeichert.'); } } catch (\Throwable $throwable) { remember_old_input($_POST); @@ -472,6 +474,7 @@ final class TenantController m.display_name AS member_name, p.name AS product_name, s.name AS source_name, + s.code AS source_code, le.id AS ledger_entry_id, CASE WHEN reversal.id IS NULL THEN 0 ELSE 1 END AS is_reversed FROM consumption_events c @@ -492,7 +495,7 @@ final class TenantController if (!$isManager) { if ($currentMember === null) { - throw new RuntimeException('Fuer diesen Benutzer ist kein aktives Mitglied im Mandanten hinterlegt.'); + throw new RuntimeException('Für diesen Benutzer ist kein aktives Mitglied in der Kaffeeliste hinterlegt.'); } $sql .= ' AND c.member_id = :member_id'; @@ -505,7 +508,7 @@ final class TenantController $bookingsStatement->execute($params); $this->view->render('tenant/bookings', [ - 'title' => 'Digitale Buchungen', + 'title' => 'Kaffee buchen', 'tenant' => $tenant, 'authState' => $authState, 'members' => $members, @@ -550,7 +553,7 @@ final class TenantController } } else { if ($currentMember === null) { - throw new RuntimeException('Fuer diesen Benutzer ist kein aktives Mitglied im Mandanten hinterlegt.'); + throw new RuntimeException('Für diesen Benutzer ist kein aktives Mitglied in der Kaffeeliste hinterlegt.'); } $memberId = (int) $currentMember['id']; @@ -614,7 +617,7 @@ final class TenantController (int) $authState['user_id'], trim((string) ($_POST['notes'] ?? '')) ); - $this->session->flash('success', 'Die Einzahlung wurde gebucht.'); + $this->session->flash('success', 'Die Einzahlung wurde gutgeschrieben.'); } } catch (\Throwable $throwable) { remember_old_input($_POST); @@ -645,7 +648,7 @@ final class TenantController $paymentsStatement->execute(['tenant_id' => $tenant['id']]); $this->view->render('tenant/payments', [ - 'title' => 'Einzahlungen', + 'title' => 'Geld einzahlen', 'tenant' => $tenant, 'authState' => $authState, 'members' => $members, @@ -688,10 +691,10 @@ final class TenantController trim((string) ($_POST['note'] ?? '')) ?: null, (int) $authState['user_id'] ); - $this->session->flash('success', 'Die Papierlisten-Position wurde gespeichert.'); + $this->session->flash('success', 'Der Eintrag aus der Papierliste wurde gespeichert.'); } elseif ($action === 'post_sheet') { $service->postPaperSheet((int) $tenant['id'], (int) ($_POST['paper_sheet_id'] ?? 0), (int) $authState['user_id']); - $this->session->flash('success', 'Die Papierliste wurde ins Ledger uebernommen.'); + $this->session->flash('success', 'Die Papierliste wurde als Buchungen übernommen.'); } elseif ($action === 'reverse_sheet') { $service->reversePaperSheet( (int) $tenant['id'], @@ -699,7 +702,7 @@ final class TenantController (int) $authState['user_id'], trim((string) ($_POST['reason'] ?? '')) ?: null ); - $this->session->flash('success', 'Die verbuchte Papierliste wurde storniert.'); + $this->session->flash('success', 'Die übernommene Papierliste wurde storniert.'); } } catch (\Throwable $throwable) { remember_old_input($_POST); @@ -745,7 +748,7 @@ final class TenantController $linesStatement->execute(['tenant_id' => $tenant['id']]); $this->view->render('tenant/paper-sheets', [ - 'title' => 'Papierlisten', + 'title' => 'Papierliste nachtragen', 'tenant' => $tenant, 'authState' => $authState, 'members' => $members, @@ -775,7 +778,7 @@ final class TenantController trim((string) ($_POST['location_label'] ?? '')) ?: null, (int) $authState['user_id'] ); - $this->session->flash('success', 'Das RFID-Geraet wurde vorbereitet. Token: ' . $token); + $this->session->flash('success', 'Der Kartenleser wurde angelegt. Das Geräte-Token wird einmalig angezeigt: ' . $token); } elseif ($action === 'assign_tag') { $service->assignTag( (int) $tenant['id'], @@ -784,7 +787,7 @@ final class TenantController trim((string) ($_POST['label'] ?? '')) ?: null, (int) $authState['user_id'] ); - $this->session->flash('success', 'Die RFID-Karte wurde zugewiesen.'); + $this->session->flash('success', 'Die Karte wurde zugewiesen.'); } } catch (\Throwable $throwable) { remember_old_input($_POST); @@ -816,7 +819,7 @@ final class TenantController $events->execute(['tenant_id' => $tenant['id']]); $this->view->render('tenant/rfid', [ - 'title' => 'RFID-Roadmap', + 'title' => 'Kartenleser einrichten', 'tenant' => $tenant, 'authState' => $authState, 'members' => $members, @@ -862,7 +865,7 @@ final class TenantController $tenant = (new TenantContext($pdo))->bySlug($tenantSlug); if (!$tenant || $tenant['status'] === 'suspended') { - throw new RuntimeException('Der Mandant ist nicht verfuegbar.'); + throw new RuntimeException('Die Kaffeeliste ist nicht verfügbar.'); } return $tenant; @@ -880,7 +883,7 @@ final class TenantController } if ($roles !== [] && !$auth->requireRole($roles)) { - $this->session->flash('error', 'Dafuer fehlen die passenden Rechte.'); + $this->session->flash('error', 'Dafür fehlen die passenden Rechte.'); Response::redirect(tenant_url($tenantSlug)); } @@ -893,7 +896,7 @@ final class TenantController if (!$this->isTenantManager($authState) && !$this->selfServiceAllowed((int) $tenant['id'])) { $auth->logout(); - $this->session->flash('error', 'Die digitale Selbstbuchung ist fuer diesen Mandanten derzeit deaktiviert.'); + $this->session->flash('error', 'Die Buchung ist für diese Kaffeeliste derzeit deaktiviert.'); Response::redirect(tenant_url($tenantSlug, 'login')); } @@ -968,18 +971,18 @@ final class TenantController ): int { if ($this->isTenantManager($authState)) { if ($postedMemberId <= 0) { - throw new RuntimeException('Bitte ein Mitglied fuer die Buchung auswaehlen.'); + throw new RuntimeException('Bitte ein Mitglied für die Buchung auswählen.'); } return $this->assertMemberIdBelongsToTenant($tenantId, $postedMemberId); } if ($currentMember === null) { - throw new RuntimeException('Fuer dieses Konto ist kein buchungsfaehiges Mitglied hinterlegt.'); + throw new RuntimeException('Für dieses Konto ist kein buchungsfähiges Mitglied hinterlegt.'); } if ($postedMemberId > 0 && $postedMemberId !== (int) $currentMember['id']) { - throw new RuntimeException('Du darfst nur fuer dein eigenes Mitglied buchen.'); + throw new RuntimeException('Sie dürfen nur für Ihr eigenes Mitglied buchen.'); } return (int) $currentMember['id']; @@ -1001,7 +1004,7 @@ final class TenantController ]); if (!$statement->fetchColumn()) { - throw new RuntimeException('Das angegebene Mitglied gehoert nicht zu diesem Mandanten.'); + throw new RuntimeException('Das angegebene Mitglied gehört nicht zu dieser Kaffeeliste.'); } return $memberId; diff --git a/app/Services/ExportService.php b/app/Services/ExportService.php index 9fef981..77bd1f1 100644 --- a/app/Services/ExportService.php +++ b/app/Services/ExportService.php @@ -271,7 +271,7 @@ final class ExportService private function assertTenantId(int $tenantId): void { if ($tenantId <= 0) { - throw new RuntimeException('Die Tenant-ID muss groesser als 0 sein.'); + throw new RuntimeException('Die Standort-ID muss größer als 0 sein.'); } } @@ -283,7 +283,7 @@ final class ExportService private function openCsvOutput(string $filename) { if (headers_sent($file, $line)) { - throw new RuntimeException(sprintf('CSV-Header koennen nicht mehr gesendet werden (%s:%d).', $file, $line)); + throw new RuntimeException(sprintf('Export-Header können nicht mehr gesendet werden (%s:%d).', $file, $line)); } $safeFilename = $this->sanitizeFilename($filename); @@ -299,7 +299,7 @@ final class ExportService $handle = fopen('php://output', 'wb'); if ($handle === false) { - throw new RuntimeException('php://output konnte nicht geoeffnet werden.'); + throw new RuntimeException('Die Exportausgabe konnte nicht geöffnet werden.'); } fwrite($handle, "\xEF\xBB\xBF"); @@ -330,7 +330,7 @@ final class ExportService }, $row); if (fputcsv($handle, $values, self::CSV_DELIMITER) === false) { - throw new RuntimeException('Die CSV-Zeile konnte nicht geschrieben werden.'); + throw new RuntimeException('Die Exportzeile konnte nicht geschrieben werden.'); } } diff --git a/app/Services/LedgerService.php b/app/Services/LedgerService.php index 7ec84a6..69ccb92 100644 --- a/app/Services/LedgerService.php +++ b/app/Services/LedgerService.php @@ -278,7 +278,7 @@ final class LedgerService $this->assertTenantMembership($tenantId, $createdByUserId); if (trim($title) === '') { - throw new RuntimeException('Bitte einen Titel fuer die Papierliste angeben.'); + throw new RuntimeException('Bitte einen Titel für die Liste angeben.'); } $statement = $this->pdo->prepare( @@ -345,11 +345,11 @@ final class LedgerService $paperSheet = $sheet->fetch(); if (!$paperSheet) { - throw new RuntimeException('Die Papierliste wurde nicht gefunden.'); + throw new RuntimeException('Die Liste wurde nicht gefunden.'); } if ($paperSheet['status'] !== 'draft') { - throw new RuntimeException('Nur Entwuerfe koennen erweitert werden.'); + throw new RuntimeException('Nur Entwürfe können erweitert werden.'); } $statement = $this->pdo->prepare( @@ -402,11 +402,11 @@ final class LedgerService $sheet = $sheetStatement->fetch(); if (!$sheet) { - throw new RuntimeException('Die Papierliste wurde nicht gefunden.'); + throw new RuntimeException('Die Liste wurde nicht gefunden.'); } if ($sheet['status'] !== 'draft') { - throw new RuntimeException('Die Papierliste wurde bereits verbucht.'); + throw new RuntimeException('Die Liste wurde bereits verbucht.'); } $linesStatement = $this->pdo->prepare( @@ -419,7 +419,7 @@ final class LedgerService $lines = $linesStatement->fetchAll(); if ($lines === []) { - throw new RuntimeException('Die Papierliste enthaelt noch keine Positionen.'); + throw new RuntimeException('Die Liste enthält noch keine Positionen.'); } foreach ($lines as $line) { @@ -431,7 +431,7 @@ final class LedgerService $line['effective_at'] ?: gmdate('Y-m-d H:i:s'), $actorUserId, (float) $line['quantity'], - $line['note'] ?: 'Papierliste', + $line['note'] ?: 'Liste', (int) $line['id'], 'sheet:' . $sheetId ); @@ -472,7 +472,7 @@ final class LedgerService $lines = $linesStatement->fetchAll(); if ($lines === []) { - throw new RuntimeException('Es wurden keine Ledger-Zeilen fuer den Vorgang gefunden.'); + throw new RuntimeException('Es wurden keine Buchungszeilen für den Vorgang gefunden.'); } $description = 'Storno: ' . $entry['description']; @@ -583,7 +583,7 @@ final class LedgerService $entryIds = $statement->fetchAll(PDO::FETCH_COLUMN); if ($entryIds === []) { - throw new RuntimeException('Zu dieser Papierliste wurden keine verbuchten Eintraege gefunden.'); + throw new RuntimeException('Zu dieser Liste wurden keine verbuchten Einträge gefunden.'); } $reversed = 0; @@ -597,13 +597,13 @@ final class LedgerService $tenantId, (int) $entryId, $actorUserId, - $reason !== null && trim($reason) !== '' ? $reason : 'Papierliste #' . $sheetId + $reason !== null && trim($reason) !== '' ? $reason : 'Liste #' . $sheetId ); $reversed++; } if ($reversed === 0) { - throw new RuntimeException('Diese Papierliste wurde bereits vollstaendig storniert.'); + throw new RuntimeException('Diese Liste wurde bereits vollständig storniert.'); } $this->audit->log($tenantId, $actorUserId, 'paper_sheet.reversed', 'paper_sheet', $sheetId, 'success', [ @@ -633,7 +633,7 @@ final class LedgerService $price = $statement->fetchColumn(); if ($price === false) { - throw new RuntimeException('Fuer das Produkt ist kein aktiver Preis hinterlegt.'); + throw new RuntimeException('Für das Produkt ist kein aktiver Preis hinterlegt.'); } return (int) $price; @@ -668,7 +668,7 @@ final class LedgerService ]); if (!$statement->fetchColumn()) { - throw new RuntimeException('Das gewaehlte Mitglied gehoert nicht zu diesem Mandanten.'); + throw new RuntimeException('Das gewählte Mitglied gehört nicht zu diesem Standort.'); } } @@ -683,7 +683,7 @@ final class LedgerService ]); if (!$statement->fetchColumn()) { - throw new RuntimeException('Das gewaehlte Produkt gehoert nicht zu diesem Mandanten.'); + throw new RuntimeException('Das gewählte Produkt gehört nicht zu dieser Kaffeeliste.'); } } @@ -703,7 +703,7 @@ final class LedgerService ]); if (!$statement->fetchColumn()) { - throw new RuntimeException('Der Benutzer ist fuer diesen Mandanten nicht aktiv.'); + throw new RuntimeException('Der Benutzer ist für diesen Standort nicht aktiv.'); } } diff --git a/app/Services/MailerService.php b/app/Services/MailerService.php index 3eea101..16e6345 100644 --- a/app/Services/MailerService.php +++ b/app/Services/MailerService.php @@ -20,11 +20,11 @@ final class MailerService $textBody = trim($textBody); if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) { - throw new RuntimeException('Die Zieladresse fuer den Mailversand ist ungueltig.'); + throw new RuntimeException('Die Zieladresse für den Mailversand ist ungültig.'); } if ($subject === '' || $textBody === '') { - throw new RuntimeException('Betreff und Inhalt fuer den Mailversand sind erforderlich.'); + throw new RuntimeException('Betreff und Inhalt für den Mailversand sind erforderlich.'); } $subjectLine = $this->encodeSubject($this->sanitizeHeaderValue($this->subjectPrefix() . $subject)); @@ -61,7 +61,7 @@ final class MailerService 'MIME-Version: 1.0', 'Content-Type: text/plain; charset=UTF-8', 'Content-Transfer-Encoding: 8bit', - 'X-Mailer: Kaffeeliste-Neustart', + 'X-Mailer: Kaffeeliste', ]; $from = trim((string) ($this->config['from'] ?? '')); diff --git a/app/Services/PasswordResetService.php b/app/Services/PasswordResetService.php index 55aa88d..05a80be 100644 --- a/app/Services/PasswordResetService.php +++ b/app/Services/PasswordResetService.php @@ -40,13 +40,13 @@ final class PasswordResetService public function createTokenForUser(int $userId, array $context = []): array { if ($userId <= 0) { - throw new RuntimeException('Der Benutzer fuer den Passwort-Reset ist ungueltig.'); + throw new RuntimeException('Der Benutzer für den Passwortwechsel ist ungültig.'); } $user = $this->findUserById($userId); if (!$user) { - throw new RuntimeException('Der Benutzer fuer den Passwort-Reset wurde nicht gefunden.'); + throw new RuntimeException('Der Benutzer für den Passwortwechsel wurde nicht gefunden.'); } $nowTs = time(); @@ -204,7 +204,7 @@ final class PasswordResetService $newPassword = (string) $newPassword; if ($token === '') { - throw new RuntimeException('Der Reset-Link ist ungueltig oder abgelaufen.'); + throw new RuntimeException('Der Link ist ungültig oder abgelaufen.'); } if (mb_strlen($newPassword) < 12) { @@ -229,7 +229,7 @@ final class PasswordResetService $resetToken = $statement->fetch(); if (!$resetToken) { - throw new RuntimeException('Der Reset-Link ist ungueltig oder abgelaufen.'); + throw new RuntimeException('Der Link ist ungültig oder abgelaufen.'); } $expiresAtTs = $this->timestampFromDatabase($resetToken['expires_at']); @@ -239,7 +239,7 @@ final class PasswordResetService || $expiresAtTs === null || $expiresAtTs < $nowTs ) { - throw new RuntimeException('Der Reset-Link ist ungueltig oder abgelaufen.'); + throw new RuntimeException('Der Link ist ungültig oder abgelaufen.'); } $updateUserStatement = $this->pdo->prepare( @@ -314,14 +314,14 @@ final class PasswordResetService private function sendResetMail(array $token): void { $name = trim((string) ($token['full_name'] ?? '')); - $greeting = $name !== '' ? 'Hallo ' . $name . ',' : 'Hallo,'; + $greeting = $name !== '' ? 'Guten Tag ' . $name . ',' : 'Guten Tag,'; $body = implode("\n\n", [ $greeting, - 'fuer dein Konto wurde ein Passwort-Reset angefragt.', - 'Bitte nutze diesen Link, um ein neues Passwort zu setzen:', + 'für Ihr Konto wurde ein Passwortwechsel angefragt.', + 'Bitte nutzen Sie diesen Link, um ein neues Passwort zu setzen:', (string) $token['reset_url'], - 'Der Link ist gueltig bis ' . (string) $token['expires_at'] . ' UTC.', - 'Falls du den Reset nicht angefragt hast, kannst du diese Nachricht ignorieren.', + 'Der Link ist gültig bis ' . (string) $token['expires_at'] . '.', + 'Falls Sie den Passwortwechsel nicht angefragt haben, können Sie diese Nachricht ignorieren.', ]); $this->mailer->send( @@ -410,7 +410,7 @@ final class PasswordResetService { $subject = trim((string) ($this->config['subject'] ?? '')); - return $subject !== '' ? $subject : 'Passwort zuruecksetzen'; + return $subject !== '' ? $subject : 'Passwort zurücksetzen'; } private function ttlMinutes(): int diff --git a/app/Services/RfidService.php b/app/Services/RfidService.php index bb835cd..e10d056 100644 --- a/app/Services/RfidService.php +++ b/app/Services/RfidService.php @@ -16,7 +16,7 @@ final class RfidService public function createDevice(int $tenantId, string $name, ?string $location, int $actorUserId): string { if (trim($name) === '') { - throw new RuntimeException('Bitte einen Geraetenamen angeben.'); + throw new RuntimeException('Bitte einen Namen für den Kartenleser angeben.'); } $token = bin2hex(random_bytes(20)); @@ -52,7 +52,7 @@ final class RfidService ]); if (!$memberStatement->fetchColumn()) { - throw new RuntimeException('Das ausgewaehlte Mitglied ist fuer RFID nicht verfuegbar.'); + throw new RuntimeException('Das ausgewählte Mitglied ist für Karten nicht verfügbar.'); } $statement = $this->pdo->prepare( @@ -89,11 +89,11 @@ final class RfidService $device = $deviceStatement->fetch(); if (!$device) { - throw new RuntimeException('Das RFID-Geraet ist unbekannt.'); + throw new RuntimeException('Der Kartenleser ist unbekannt.'); } if (($device['status'] ?? '') !== 'active') { - throw new RuntimeException('Das RFID-Geraet ist nicht aktiv.'); + throw new RuntimeException('Der Kartenleser ist nicht aktiv.'); } $status = 'received'; diff --git a/app/Services/SetupService.php b/app/Services/SetupService.php index 1710aa1..8809d6c 100644 --- a/app/Services/SetupService.php +++ b/app/Services/SetupService.php @@ -31,12 +31,12 @@ final class SetupService foreach ($required as $field) { if (trim((string) ($input[$field] ?? '')) === '') { - throw new RuntimeException('Bitte alle Pflichtfelder ausfuellen.'); + throw new RuntimeException('Bitte alle Pflichtfelder ausfüllen.'); } } if (!filter_var($input['admin_email'], FILTER_VALIDATE_EMAIL)) { - throw new RuntimeException('Die Admin-E-Mail ist ungueltig.'); + throw new RuntimeException('Die Admin-E-Mail ist ungültig.'); } if (mb_strlen($input['admin_password']) < 12) { @@ -171,7 +171,7 @@ final class SetupService $rfidSecret = bin2hex(random_bytes(24)); $lines = [ - 'APP_NAME="Kaffeekasse SaaS"', + 'APP_NAME="Kaffeeliste"', 'APP_ENV=production', 'APP_DEBUG=0', 'APP_URL=' . $appUrl, @@ -206,14 +206,14 @@ final class SetupService '', 'MAIL_FROM=' . $mailFrom, 'MAIL_REPLY_TO=' . $mailFrom, - 'MAIL_SUBJECT_PREFIX="[Kaffeekasse] "', + 'MAIL_SUBJECT_PREFIX="[Kaffeeliste] "', 'MAIL_LOG_FALLBACK=1', 'MAIL_LOG_FILE=storage/logs/mail.log', '', 'PASSWORD_RESET_URL=' . $appUrl . '/reset-password?token={{token}}', 'PASSWORD_RESET_TTL_MINUTES=60', 'PASSWORD_RESET_MAX_ACTIVE_TOKENS=3', - 'PASSWORD_RESET_SUBJECT="Passwort zuruecksetzen"', + 'PASSWORD_RESET_SUBJECT="Passwort zurücksetzen"', '', 'RATE_LIMIT_LOGIN_MAX_ATTEMPTS=5', 'RATE_LIMIT_LOGIN_WINDOW_SECONDS=900', @@ -242,7 +242,7 @@ final class SetupService } if (!filter_var($mailFrom, FILTER_VALIDATE_EMAIL)) { - throw new RuntimeException('Die Mail-Absenderadresse ist ungueltig.'); + throw new RuntimeException('Die Mail-Absenderadresse ist ungültig.'); } return $mailFrom; diff --git a/app/Services/TenantRegistrationService.php b/app/Services/TenantRegistrationService.php index 582b76e..63d4369 100644 --- a/app/Services/TenantRegistrationService.php +++ b/app/Services/TenantRegistrationService.php @@ -28,7 +28,7 @@ final class TenantRegistrationService } if (!filter_var($ownerEmail, FILTER_VALIDATE_EMAIL)) { - throw new RuntimeException('Die E-Mail-Adresse ist ungueltig.'); + throw new RuntimeException('Die E-Mail-Adresse ist ungültig.'); } if (mb_strlen($ownerPassword) < 12) { @@ -52,7 +52,7 @@ final class TenantRegistrationService $existingUser->execute(['email' => $ownerEmail]); if ($existingUser->fetchColumn()) { - throw new RuntimeException('Die E-Mail-Adresse ist bereits vorhanden. Fuer weitere Tenants bitte spaeter Einladungen nutzen.'); + throw new RuntimeException('Die E-Mail-Adresse ist bereits vorhanden. Bitte verwenden Sie eine andere Adresse.'); } $now = gmdate('Y-m-d H:i:s'); @@ -131,10 +131,10 @@ final class TenantRegistrationService ); foreach ([ - ['digital_self', 'Digitale Selbstbuchung', 'digital'], - ['paper_sheet', 'Papierliste / Nacherfassung', 'paper'], - ['admin_backoffice', 'Backoffice-Buchung', 'admin'], - ['rfid_reader', 'RFID-Geraet', 'rfid'], + ['digital_self', 'Selbst gebucht', 'digital'], + ['paper_sheet', 'Papierliste / Nachtrag', 'paper'], + ['admin_backoffice', 'Nachgetragen durch Verwaltung', 'admin'], + ['rfid_reader', 'Kartenleser', 'rfid'], ] as [$code, $sourceName, $channel]) { $sourceInsert->execute([ 'tenant_id' => $tenantId, diff --git a/config/app.php b/config/app.php index ba71e26..d954be6 100644 --- a/config/app.php +++ b/config/app.php @@ -1,7 +1,7 @@ $_ENV['APP_NAME'] ?? 'Kaffeekasse SaaS', + 'name' => $_ENV['APP_NAME'] ?? 'Kaffeeliste', 'env' => $_ENV['APP_ENV'] ?? 'production', 'debug' => (($_ENV['APP_DEBUG'] ?? '0') === '1'), 'url' => rtrim($_ENV['APP_URL'] ?? 'http://localhost:8080', '/'), @@ -20,7 +20,7 @@ return [ 'mail' => [ 'from' => trim($_ENV['MAIL_FROM'] ?? 'noreply@example.com'), 'reply_to' => trim($_ENV['MAIL_REPLY_TO'] ?? ''), - 'subject_prefix' => $_ENV['MAIL_SUBJECT_PREFIX'] ?? '[Kaffeekasse] ', + 'subject_prefix' => $_ENV['MAIL_SUBJECT_PREFIX'] ?? '[Kaffeeliste] ', 'log_fallback' => (($_ENV['MAIL_LOG_FALLBACK'] ?? '1') === '1'), 'log_file' => trim($_ENV['MAIL_LOG_FILE'] ?? ''), ], @@ -29,7 +29,7 @@ return [ ?? rtrim($_ENV['APP_URL'] ?? 'http://localhost:8080', '/') . '/reset-password?token={{token}}', 'ttl_minutes' => max(5, (int) ($_ENV['PASSWORD_RESET_TTL_MINUTES'] ?? 60)), 'max_active_tokens' => max(1, (int) ($_ENV['PASSWORD_RESET_MAX_ACTIVE_TOKENS'] ?? 3)), - 'subject' => $_ENV['PASSWORD_RESET_SUBJECT'] ?? 'Passwort zuruecksetzen', + 'subject' => $_ENV['PASSWORD_RESET_SUBJECT'] ?? 'Passwort zurücksetzen', ], 'rate_limits' => [ 'login' => [ diff --git a/public/assets/app.css b/public/assets/app.css index e1d22cd..adb879c 100644 --- a/public/assets/app.css +++ b/public/assets/app.css @@ -1,44 +1,54 @@ :root { - --bg: #f6efe6; - --bg-accent: #efe0cd; - --surface: rgba(255, 252, 248, 0.92); - --surface-strong: #fffaf2; - --text: #1f140f; - --muted: #6b544a; - --border: rgba(86, 48, 26, 0.14); - --primary: #8f4518; - --primary-strong: #6f2f0e; - --secondary: #124f47; - --success: #e7f7ef; - --danger: #ffe3df; - --shadow: 0 20px 60px rgba(86, 48, 26, 0.12); - --radius: 24px; + --bg: #f5f6f8; + --bg-accent: #eef2f7; + --surface: #ffffff; + --surface-subtle: #f9fafb; + --surface-strong: #ffffff; + --text: #111827; + --muted: #6b7280; + --border: #d9dee7; + --border-strong: #b8c0cc; + --primary: #0f62fe; + --primary-strong: #0043ce; + --secondary: #344054; + --success: #ecfdf3; + --success-border: #a6e9c5; + --danger: #fef2f2; + --danger-border: #fecaca; + --info: #eff6ff; + --info-border: #bfdbfe; + --shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 8px 24px rgba(16, 24, 40, 0.04); + --radius: 8px; } * { box-sizing: border-box; } +html { + color-scheme: light; +} + body { margin: 0; min-height: 100vh; color: var(--text); - font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif; - background: - radial-gradient(circle at top left, rgba(143, 69, 24, 0.18), transparent 26rem), - radial-gradient(circle at top right, rgba(18, 79, 71, 0.14), transparent 24rem), - linear-gradient(160deg, var(--bg) 0%, #fcf7f0 48%, var(--bg-accent) 100%); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; + font-size: 16px; + line-height: 1.55; + background: linear-gradient(180deg, #fbfcfe 0%, var(--bg) 100%); +} + +body, +button, +input, +select, +textarea { + letter-spacing: 0; } .page-backdrop { - position: fixed; - inset: 0; - pointer-events: none; - background: - linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px), - linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px); - background-size: 3rem 3rem; - mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.4), transparent 70%); + display: none; } .shell { @@ -50,22 +60,23 @@ body { position: sticky; top: 0; z-index: 20; - backdrop-filter: blur(18px); - background: rgba(246, 239, 230, 0.72); + background: rgba(255, 255, 255, 0.88); border-bottom: 1px solid var(--border); + backdrop-filter: saturate(1.2) blur(18px); } .header-inner, .footer-grid, .button-row, .metric-row, -.top-nav, .inline-head, .two-column, .grid-three, .content-grid, .plan-grid, -.metric-cards { +.metric-cards, +.action-strip, +.section-actions { display: grid; gap: 1rem; } @@ -73,22 +84,22 @@ body { .header-inner { grid-template-columns: auto 1fr; align-items: center; - padding: 1rem 0; + padding: 0.9rem 0; } .brand { display: inline-flex; align-items: center; - gap: 0.85rem; + gap: 0.75rem; + min-width: 0; color: inherit; text-decoration: none; } -.brand strong, -.hero h1, -.page-head h1, -.section-head h2 { - letter-spacing: -0.03em; +.brand strong { + display: block; + font-size: 0.98rem; + font-weight: 700; } .brand small, @@ -97,37 +108,49 @@ body { .feature-list, .metric-row span, .top-nav a, -.site-footer p { +.site-footer p, +.form-note, +.table-empty { color: var(--muted); } .brand-mark { display: inline-grid; place-items: center; - width: 2.75rem; - height: 2.75rem; - border-radius: 0.9rem; - background: linear-gradient(135deg, var(--primary), #c07a3d); - color: white; + width: 2.35rem; + height: 2.35rem; + border-radius: var(--radius); + background: var(--text); + color: #ffffff; + font-size: 0.82rem; font-weight: 700; + flex: 0 0 auto; } .top-nav { - grid-auto-flow: column; - justify-content: end; + display: flex; + flex-wrap: wrap; + justify-content: flex-end; align-items: center; + gap: 0.35rem; } .top-nav a { + display: inline-flex; + align-items: center; + min-height: 2.35rem; + padding: 0.5rem 0.7rem; + border-radius: var(--radius); text-decoration: none; - padding: 0.65rem 0.9rem; - border-radius: 999px; + font-size: 0.94rem; + font-weight: 600; + white-space: nowrap; } .top-nav a.active, .top-nav a:hover { - background: rgba(18, 79, 71, 0.08); - color: var(--secondary); + background: var(--info); + color: var(--primary-strong); } .main-shell { @@ -151,40 +174,60 @@ body { .hero-card, .card, .form-card, -.table-card { +.table-card, +.plan-card, +.timeline, +.metric-card, +.quick-action { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); } -.hero-copy { - padding: 2.4rem; +.hero-copy, +.hero-card { + padding: 2rem; } .hero-card { - padding: 2rem; align-self: stretch; - background: - linear-gradient(180deg, rgba(255,255,255,0.84), rgba(255,250,242,0.94)), - linear-gradient(135deg, rgba(18,79,71,0.12), transparent 50%); +} + +h1, +h2, +h3, +p { + margin-top: 0; } .hero h1, .page-head h1 { - font-size: clamp(2.2rem, 4vw, 4.6rem); - line-height: 0.96; + max-width: 13ch; margin: 0.6rem 0 1rem; + font-size: 3rem; + line-height: 1.05; + font-weight: 760; } -.lead { - font-size: 1.15rem; +.section-head h2, +.card h2, +.hero-card h2 { + font-size: 1.35rem; + line-height: 1.25; +} + +.lead, +.page-head p { + max-width: 58rem; + color: var(--secondary); + font-size: 1.08rem; line-height: 1.7; - max-width: 56rem; } .button-row, -.metric-row { +.metric-row, +.section-actions { grid-auto-flow: column; justify-content: start; } @@ -193,43 +236,73 @@ body { display: inline-flex; justify-content: center; align-items: center; - padding: 0.95rem 1.25rem; - border-radius: 999px; + min-height: 2.75rem; + padding: 0.72rem 1rem; + border-radius: var(--radius); border: 1px solid transparent; text-decoration: none; - font-weight: 700; + font-weight: 650; cursor: pointer; + transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease, box-shadow 120ms ease; } .button-primary { - color: white; - background: linear-gradient(135deg, var(--primary), var(--primary-strong)); + color: #ffffff; + background: var(--primary); +} + +.button-primary:hover { + background: var(--primary-strong); } .button-secondary { - color: var(--secondary); - border-color: rgba(18, 79, 71, 0.24); - background: rgba(18, 79, 71, 0.08); + color: var(--text); + border-color: var(--border); + background: var(--surface); +} + +.button-secondary:hover { + border-color: var(--border-strong); + background: var(--surface-subtle); +} + +.button:disabled, +.button[aria-disabled="true"] { + opacity: 0.55; + cursor: not-allowed; } .metric-row { list-style: none; padding: 0; - margin: 1.4rem 0 0; - align-items: end; + margin: 1.5rem 0 0; + align-items: start; } -.metric-row strong { +.metric-row li { + max-width: 15rem; +} + +.metric-row strong, +.metric-card strong, +.stats-panel strong { display: block; - font-size: 1.7rem; + font-size: 1.6rem; + line-height: 1.15; +} + +.metric-row span, +.metric-card span, +.stats-panel span { + display: block; + margin-top: 0.25rem; } .eyebrow { display: inline-block; - text-transform: uppercase; - letter-spacing: 0.18em; - font-size: 0.72rem; - color: var(--primary); + margin-bottom: 0.45rem; + color: var(--primary-strong); + font-size: 0.84rem; font-weight: 700; } @@ -237,35 +310,40 @@ body { .stack-list, .step-list { margin: 1rem 0 0; - padding-left: 1.2rem; + padding-left: 1.15rem; +} + +.feature-list li, +.step-list li { + margin: 0.45rem 0; } .stats-panel, .metric-cards, .content-grid, .plan-grid, -.grid-three { +.grid-three, +.action-strip { margin-top: 1.5rem; } .stats-panel { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 0.85rem; + gap: 0.8rem; } .stats-panel div, .metric-card { padding: 1rem; - border-radius: 1rem; - background: rgba(255,255,255,0.74); + background: var(--surface-subtle); border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: none; } -.metric-card strong, -.stats-panel strong { - display: block; - font-size: 1.7rem; +.metric-card { + min-height: 7rem; } .content-grid, @@ -277,13 +355,14 @@ body { .card, .form-card, .table-card, -.timeline { - padding: 1.4rem; +.timeline, +.plan-card { + padding: 1.25rem; } .highlighted { - border-color: rgba(143, 69, 24, 0.38); - background: linear-gradient(180deg, rgba(255, 251, 244, 0.98), rgba(250, 236, 219, 0.9)); + border-color: var(--primary); + box-shadow: 0 0 0 1px rgba(15, 98, 254, 0.2), var(--shadow); } .page-head { @@ -292,14 +371,17 @@ body { .inline-head { grid-template-columns: 1fr auto; - align-items: center; + align-items: start; } -.form-card, -.table-card { +.form-card { overflow: hidden; } +.table-card { + overflow-x: auto; +} + .form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -314,7 +396,7 @@ label { display: grid; gap: 0.45rem; margin-bottom: 1rem; - font-weight: 700; + font-weight: 650; } input, @@ -328,30 +410,59 @@ input, select, textarea { width: 100%; - padding: 0.85rem 1rem; - border-radius: 1rem; - border: 1px solid rgba(86, 48, 26, 0.18); - background: rgba(255,255,255,0.95); + min-height: 2.75rem; + padding: 0.72rem 0.85rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: #ffffff; + color: var(--text); +} + +input::placeholder, +textarea::placeholder { + color: #98a2b3; +} + +input:focus, +select:focus, +textarea:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(15, 98, 254, 0.16); +} + +a:focus-visible, +button:focus-visible, +.button:focus-visible, +input:focus-visible, +select:focus-visible, +textarea:focus-visible { + outline: 3px solid rgba(15, 98, 254, 0.35); + outline-offset: 2px; } table { width: 100%; + min-width: 42rem; border-collapse: collapse; } th, td { - padding: 0.95rem 0.8rem; - border-bottom: 1px solid rgba(86, 48, 26, 0.08); + padding: 0.82rem 0.75rem; + border-bottom: 1px solid var(--border); text-align: left; vertical-align: top; } th { - font-size: 0.82rem; - text-transform: uppercase; - letter-spacing: 0.12em; color: var(--muted); + font-size: 0.86rem; + font-weight: 700; +} + +tbody tr:hover { + background: var(--surface-subtle); } .stack-list { @@ -363,68 +474,135 @@ th { display: grid; gap: 0.2rem; padding: 0.85rem 0; - border-bottom: 1px solid rgba(86, 48, 26, 0.08); + border-bottom: 1px solid var(--border); } -.flash { +.stack-list li:last-child { + border-bottom: 0; +} + +.flash, +.alert { margin-bottom: 1rem; - padding: 1rem 1.2rem; - border-radius: 1rem; - font-weight: 700; + padding: 0.9rem 1rem; + border-radius: var(--radius); + font-weight: 650; } .flash-success { background: var(--success); - border: 1px solid rgba(18, 79, 71, 0.16); + border: 1px solid var(--success-border); } -.flash-error { +.flash-error, +.alert-error { background: var(--danger); - border: 1px solid rgba(143, 69, 24, 0.16); -} - -.alert { - margin-bottom: 1rem; - padding: 1rem 1.2rem; - border-radius: 1rem; - font-weight: 700; + border: 1px solid var(--danger-border); + color: #7f1d1d; } .alert-info { - background: rgba(18, 79, 71, 0.08); - border: 1px solid rgba(18, 79, 71, 0.16); - color: var(--secondary); -} - -.alert-error { - background: var(--danger); - border: 1px solid rgba(143, 69, 24, 0.16); - color: var(--primary-strong); + background: var(--info); + border: 1px solid var(--info-border); + color: #1e3a8a; } .code-block { overflow: auto; padding: 1rem; - border-radius: 1rem; - background: #201814; - color: #fef0df; + border-radius: var(--radius); + background: #111827; + color: #f9fafb; + font-size: 0.9rem; } .text-link { - color: var(--secondary); + color: var(--primary-strong); + font-weight: 650; text-decoration: none; } +.text-link:hover { + text-decoration: underline; +} + .site-footer { padding: 0 0 2rem; } .footer-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); - padding: 1.5rem; - background: rgba(255,255,255,0.64); + padding: 1.25rem 0; + border-top: 1px solid var(--border); +} + +.action-strip { + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin-bottom: 1.5rem; +} + +.quick-action { + display: grid; + align-content: start; + gap: 0.25rem; + min-height: 7rem; + padding: 1rem; + color: inherit; + text-decoration: none; +} + +.quick-action strong { + font-size: 1rem; +} + +.quick-action span { + color: var(--muted); + font-size: 0.92rem; +} + +.quick-action.primary { + border-color: rgba(15, 98, 254, 0.35); + background: var(--info); +} + +.quick-action:hover { + border-color: var(--primary); +} + +.section-actions { + margin: 1rem 0; +} + +.form-note { + margin: 0 0 1rem; + padding: 0.85rem 1rem; border: 1px solid var(--border); border-radius: var(--radius); + background: var(--surface-subtle); +} + +.status-pill { + display: inline-flex; + align-items: center; + min-height: 1.75rem; + padding: 0.2rem 0.55rem; + border-radius: var(--radius); + background: var(--surface-subtle); + border: 1px solid var(--border); + color: var(--secondary); + font-size: 0.86rem; + font-weight: 650; +} + +.inline-form { + margin: 0.65rem 0 0; +} + +code { + padding: 0.12rem 0.28rem; + border-radius: 5px; + background: var(--surface-subtle); + border: 1px solid var(--border); } @media (max-width: 960px) { @@ -436,18 +614,59 @@ th { .grid-three, .footer-grid, .form-grid, - .inline-head { + .inline-head, + .action-strip { grid-template-columns: 1fr; } - .top-nav, - .button-row, - .metric-row { - grid-auto-flow: row; - justify-content: start; - } - .header-inner { grid-template-columns: 1fr; + gap: 0.75rem; + } + + .top-nav { + justify-content: flex-start; + flex-wrap: nowrap; + overflow-x: auto; + padding-bottom: 0.1rem; + scrollbar-width: thin; + } + + .button-row, + .metric-row, + .section-actions { + grid-auto-flow: row; + justify-content: stretch; + } + + .button-row .button, + .section-actions .button { + width: 100%; + } + + .hero h1, + .page-head h1 { + max-width: 100%; + font-size: 2.15rem; + } + + .hero-copy, + .hero-card { + padding: 1.25rem; + } + + .stats-panel { + grid-template-columns: 1fr; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; } } diff --git a/resources/views/admin/index.php b/resources/views/admin/index.php index b485728..918f461 100644 --- a/resources/views/admin/index.php +++ b/resources/views/admin/index.php @@ -2,9 +2,9 @@
- Betrieb -

Plattform-Dashboard

-

Ueberblick ueber Mandanten, Paketstand und direkte Einstiege in die Kundenumgebungen.

+ Plattform +

Kaffeelisten verwalten

+

Überblick über Kaffeelisten, Pakete und direkte Einstiege in die jeweiligen Arbeitsbereiche.

@@ -16,7 +16,7 @@ - + @@ -32,10 +32,10 @@ /t/ - + - + diff --git a/resources/views/admin/login.php b/resources/views/admin/login.php index cdbcaec..cfcf4b3 100644 --- a/resources/views/admin/login.php +++ b/resources/views/admin/login.php @@ -5,33 +5,33 @@ require __DIR__ . '/../partials/layout-top.php';
Plattformzugang -

Admin-Login

-

Fuer Pilotbetreuung, Mandantenuebersicht und Betriebsaufgaben. Mitglieder nutzen dafuer nicht diese Seite, sondern den jeweiligen Standort-Login.

+

Kaffeelisten verwalten

+

Dieser Bereich ist für Einrichtung, Standorte und Betriebsaufgaben vorgesehen.

- -

Passwort vergessen? Reset anstossen

+ +

Passwort vergessen? Passwort zurücksetzen

diff --git a/resources/views/auth/reset-confirm.php b/resources/views/auth/reset-confirm.php index c9f58fd..6ad0ef2 100644 --- a/resources/views/auth/reset-confirm.php +++ b/resources/views/auth/reset-confirm.php @@ -20,11 +20,11 @@ if ($tenantName === '') { } if ($tenantName === '' && $tenantSlug !== '') { - $tenantName = 'Standort ' . $tenantSlug; + $tenantName = 'Kaffeeliste ' . $tenantSlug; } if ($tenantName === '') { - $tenantName = 'Standortzugang'; + $tenantName = 'Kaffeelisten-Zugang'; } $token = trim((string) ($token ?? old('token'))); @@ -49,15 +49,15 @@ require __DIR__ . '/../partials/layout-top.php'; ?>
- Reset bestaetigen + Passwort erneuern

Neues Passwort setzen

- So bleibt der Login-Reset von PIN, RFID und sonstigen Standortdaten getrennt. + ? 'Legen Sie hier ein neues Passwort für die Plattformverwaltung fest.' + : 'Legen Sie hier ein neues Passwort für den Zugang von ' . $tenantName . ' fest.') + : 'Hier wird nur mit einem gültigen Link ein neues Passwort gesetzt.') ?> + PINs und Karten bleiben davon unberührt.

@@ -75,9 +75,9 @@ require __DIR__ . '/../partials/layout-top.php';

- + - Gueltig bis UTC. + Gültig bis .

@@ -89,29 +89,29 @@ require __DIR__ . '/../partials/layout-top.php'; Passwort wiederholen - -

Nach erfolgreicher Bestaetigung soll dieser Flow wieder in den passenden Login zurueckfuehren.

-

Zurueck zum Login

+ +

Danach geht es zurück zur Anmeldung.

+

Zurück zum Login

-

Reset-Link neu anfordern

+

Neuen Link anfordern

+ ? 'Öffnen Sie den Link aus der E-Mail, um hier ein neues Passwort zu setzen.' + : 'Dieser Link ist ungültig oder abgelaufen. Bitte fordern Sie einen neuen Link an.') ?>

-

Neuen Reset-Link anfordern

-

Zurueck zum Login

+

Neuen Link anfordern

+

Zurück zum Login

diff --git a/resources/views/auth/reset-request.php b/resources/views/auth/reset-request.php index fc99469..7aa9a23 100644 --- a/resources/views/auth/reset-request.php +++ b/resources/views/auth/reset-request.php @@ -20,28 +20,27 @@ if ($tenantName === '') { } if ($tenantName === '' && $tenantSlug !== '') { - $tenantName = 'Standort ' . $tenantSlug; + $tenantName = 'Kaffeeliste ' . $tenantSlug; } if ($tenantName === '') { - $tenantName = 'Standortzugang'; + $tenantName = 'Kaffeelisten-Zugang'; } $loginUrl = $scope === 'tenant' ? ($tenantSlug !== '' ? tenant_url($tenantSlug, 'login') : url('/')) : url('/admin/login'); -$emailLabel = $scope === 'tenant' ? 'Login-E-Mail' : 'Admin-E-Mail'; +$emailLabel = $scope === 'tenant' ? 'E-Mail-Adresse' : 'Admin-E-Mail'; require __DIR__ . '/../partials/layout-top.php'; ?>
Passwort vergessen -

Reset-Anfrage vorbereiten

+

Neues Passwort anfordern

- Im Pilotbetrieb kann die Bestaetigung noch manuell begleitet werden. + ? 'Fordern Sie einen Link für den Zugang zur Plattformverwaltung an.' + : 'Fordern Sie einen Link für den Zugang von ' . $tenantName . ' an.') ?>

@@ -61,29 +60,29 @@ require __DIR__ . '/../partials/layout-top.php'; - -

Nach dem Versand soll der Link wieder auf diese Strecke zurueckfuehren und danach zum passenden Login.

-

Zurueck zum Login

+ +

Nach dem Klick auf den Link können Sie ein neues Passwort festlegen.

+

Zurück zum Login

diff --git a/resources/views/home/index.php b/resources/views/home/index.php index 8c9ab79..897652c 100644 --- a/resources/views/home/index.php +++ b/resources/views/home/index.php @@ -2,37 +2,37 @@
- Pilotbetrieb fuer Teams und Vereine -

Die digitale Kaffeeliste ist bereit fuer echte Pilotstandorte, nicht fuer leere Demo-Versprechen.

-

Aktuell wird der Ablauf mit ausgewaehlten Teams verfeinert: Pilotmandanten koennen sich registrieren, Mitglieder fuer die Selbstbuchung anlegen und Backoffice-Prozesse fuer Papierlisten, Einzahlungen und spaetere RFID-Anbindung realistisch testen.

+ Digitale Kaffeeliste für Teams, Vereine und Standorte +

Kaffeekasse verwalten, ohne Zettel und Tabellen auseinanderzuhalten.

+

Mitglieder buchen ihren Kaffee selbst, Einzahlungen landen am richtigen Konto und Papierlisten können später sauber nacherfasst werden. So sehen Verantwortliche jederzeit, wer gebucht, bezahlt oder noch offen hat.

    -
  • 1gemeinsames Ledger fuer Selbstbuchung, Papier und spaetere RFID-Events
  • -
  • Pilotklare Trennung zwischen Member-Self-Service und Standort-Backoffice
  • -
  • 100%PHP/MySQL-Betrieb fuer klassisches Webhosting
  • +
  • DigitalKaffee direkt selbst oder durch die Verwaltung buchen
  • +
  • PapierStrichlisten nachtragen und gesammelt abschließen
  • +
  • SaldoOffene Beträge pro Person nachvollziehen
@@ -40,53 +40,53 @@
- Heute stabil -

Eine Buchungslogik fuer reale Nutzung

-

Jede Kaffee- oder Tee-Buchung landet im selben Ledger. Ob jemand digital bucht, ob das Standortteam eine Papierliste erfasst oder spaeter ein RFID-Leser Events einspeist: Salden, Historie und Auswertungen bleiben konsistent.

+ Buchungen +

Aus jedem Strich wird ein nachvollziehbarer Vorgang

+

Ob online gebucht, von der Verwaltung erfasst oder aus einer Papierliste übernommen: Jede Buchung landet im gemeinsamen Verlauf und wirkt sich direkt auf den Mitgliedssaldo aus.

- Pilotfokus -

Self-Service fuer Mitglieder, Backoffice fuers Standortteam

-

Der Pilot trennt bewusst zwischen einfacher Selbstbuchung und administrativen Seiten. Dadurch wirkt der Zugang fuer Mitglieder alltagstauglich, waehrend Produkte, Einzahlungen und Korrekturen beim Standortteam bleiben.

+ Verantwortung +

Mitglieder buchen selbst, die Kaffeekasse bleibt geordnet

+

Mitglieder erfassen ihren Verbrauch schnell selbst. Verantwortliche pflegen Preise, buchen Einzahlungen, korrigieren Fehler und sehen offene Beträge pro Kaffeeliste.

- Rahmen -

Ehrlicher Rollout statt Schein-Automatisierung

-

Reset-Flow, Exporte und Korrekturhilfen werden gerade an echten Pilotablaeufen geschliffen. Gleichzeitig bleibt der technische Kern schlank genug fuer klassisches Webhosting mit PHP, MySQL und Webroot auf `public/`.

+ Papierliste +

Papier bleibt möglich, die Abrechnung wird trotzdem sauber

+

Wochen- oder Monatslisten können weiter genutzt, später nachgetragen und gesammelt abgeschlossen werden. So verschwinden keine Zettel und keine offenen Beträge mehr im Chaos.

- Pilotstufen -

Die Startkonfiguration im Pilot, nicht die finale Vermarktung

+ Pakete +

Vom digitalen Start bis zur erweiterten Kaffeekasse

Starter

-

Fuer kleine Teams mit digitaler Buchung und moeglichst wenig Abstimmungsaufwand im Alltag.

+

Für Teams, die ihre Kaffeekasse zuerst digital und ohne Zusatzhardware führen wollen.

    -
  • Mitglieder, Produkte und Selbstbuchung
  • -
  • Schlanke Navigation fuer Member-Logins
  • -
  • Schneller Einstieg fuer den ersten Pilotstandort
  • +
  • Mitglieder, Produkte und Preise verwalten
  • +
  • Buchungen und Einzahlungen digital erfassen
  • +
  • Offene Beträge pro Person einsehen

Team

-

Fuer Organisationen, die Papierlisten und digitale Prozesse im Pilot parallel fahren.

+

Für Organisationen, die digitale Buchungen und Papierlisten parallel nutzen.

    -
  • Papierlisten-Workflow mit Nacherfassung
  • -
  • Backoffice fuer Einzahlungen und Korrekturen
  • -
  • Vorbereitung fuer spaetere Export- und Supportpfade
  • +
  • Papierlisten nacherfassen und gesammelt buchen
  • +
  • Einzahlungen, Korrekturen und offene Beträge verwalten
  • +
  • Buchungen als Tabelle exportieren

Business

-

Fuer Standorte mit RFID-Roadmap, hoeheren Betriebsanforderungen und fruehem Integrationsbedarf.

+

Für Standorte, die später Kartenleser oder verbindlichere Abläufe ergänzen möchten.

    -
  • RFID-Devices, Karten und Intake-API
  • -
  • Release-, Backup- und Deployment-Rahmen
  • -
  • Frueher Blick auf Betriebs- und Supportprozesse
  • +
  • Kartenleser vorbereiten und Karten zuweisen
  • +
  • RFID-Ereignisse standortbezogen empfangen
  • +
  • Betriebs-, Sicherungs- und Supportprozesse planen
@@ -94,15 +94,15 @@
- Pilotablauf -

So startet ein neuer Standort realistisch

+ Einrichtung +

In wenigen Schritten zur nutzbaren Kaffeeliste

  1. Plattform installieren und ersten Admin anlegen.
  2. -
  3. Pilotmandant registrieren, Owner-Onboarding abschliessen und Produkte vorbelegen.
  4. -
  5. Mitglieder fuer Selbstbuchung anlegen und Standort-Backoffice abstimmen.
  6. -
  7. Digitale Buchungen und Papierlisten parallel im Team einsetzen.
  8. -
  9. RFID-Geraete spaeter ueber die Event-Inbox anbinden, ohne das Kernledger umzubauen.
  10. +
  11. Kaffeeliste mit verantwortlicher Person registrieren.
  12. +
  13. Mitglieder, Produkte und Preise anlegen.
  14. +
  15. Buchungen und Einzahlungen im Alltag erfassen.
  16. +
  17. Papierlisten nachtragen oder Kartenleser später ergänzen.
diff --git a/resources/views/home/register.php b/resources/views/home/register.php index 96a3209..5f0d445 100644 --- a/resources/views/home/register.php +++ b/resources/views/home/register.php @@ -1,24 +1,24 @@
- Pilot-Onboarding -

Pilotmandant registrieren

-

Dieses Formular legt einen echten Teststandort an. Bitte nur verwenden, wenn Organisation, Ansprechpartner und Startpreis fuer den Pilotbetrieb abgestimmt sind.

+ Neue Kaffeeliste einrichten +

Kaffeeliste registrieren

+

Legen Sie eine Kaffeeliste für Ihr Büro, Team oder Ihren Verein an. Danach können Sie Mitglieder, Produkte, Einzahlungen und Papierlisten verwalten.

- +
diff --git a/resources/views/install/index.php b/resources/views/install/index.php index 249cf41..a2a8c89 100644 --- a/resources/views/install/index.php +++ b/resources/views/install/index.php @@ -3,7 +3,7 @@
Setup

Plattform installieren

-

Der Installer schreibt `.env`, legt das Datenbankschema an und erstellt den ersten Plattform-Admin. Danach ist das Self-Service-Onboarding fuer Mandanten freigeschaltet.

+

Die Installation richtet Datenbank, Grundeinstellungen und den ersten Verwaltungszugang ein. Danach können Kaffeelisten eingerichtet werden.

- Datenbankoptionen: Sie können MySQL oder SQLite verwenden. + Datenbankoptionen: Sie können MySQL oder SQLite verwenden. Für SQLite geben Sie "sqlite" als DB-Host ein.
@@ -73,7 +73,7 @@ - + diff --git a/resources/views/partials/layout-bottom.php b/resources/views/partials/layout-bottom.php index c9451cc..cb02935 100644 --- a/resources/views/partials/layout-bottom.php +++ b/resources/views/partials/layout-bottom.php @@ -1,13 +1,14 @@ +
diff --git a/resources/views/partials/layout-top.php b/resources/views/partials/layout-top.php index 6adc812..669c548 100644 --- a/resources/views/partials/layout-top.php +++ b/resources/views/partials/layout-top.php @@ -10,13 +10,15 @@ $hasTenantAccess = $tenantSlug !== '' && is_array($authState) && (($authState['tenant_slug'] ?? null) === $tenantSlug); $isTenantAdmin = in_array($tenantRole, ['owner', 'admin'], true); +$configuredName = trim((string) ($config['name'] ?? '')); +$brandName = $configuredName !== '' ? $configuredName : 'Kaffeeliste'; ?> - <?= e($title ?? $config['name']) ?> + <?= e($title ?? $brandName) ?> @@ -24,40 +26,40 @@ $isTenantAdmin = in_array($tenantRole, ['owner', 'admin'], true);
-
+
-
+ diff --git a/resources/views/tenant/bookings.php b/resources/views/tenant/bookings.php index 9fd85b9..1ad9cf9 100644 --- a/resources/views/tenant/bookings.php +++ b/resources/views/tenant/bookings.php @@ -4,44 +4,46 @@ $isSelfService = $tenantRole === 'member'; $selectedMemberId = (string) old('member_id'); $selectedProductId = (string) old('product_id'); $selectedSourceCode = (string) old('source_code', 'digital_self'); +$sourceLabels = [ + 'digital_self' => 'Selbst gebucht', + 'paper_sheet' => 'Papierliste', + 'admin_backoffice' => 'Nachgetragen durch Verwaltung', + 'rfid_reader' => 'Kartenleser', +]; require __DIR__ . '/../partials/layout-top.php'; ?>
- -

+ +

Kaffee buchen

-

Diese Seite ist auf taegliche Member-Self-Service-Buchungen zugeschnitten. Korrekturen, Sammelbuchungen und Exporte bleiben im Pilot vorerst beim Standortteam.

+

Wählen Sie, was Sie genommen haben. Die Buchung landet direkt auf Ihrem Kaffeekonto.

-

Browser, Tablet und Kiosk landen im selben Ledger. Das Standortteam kann hier Selbstbuchung und Backoffice-Buchungen sichtbar trennen.

+

Wählen Sie Mitglied, Produkt und Menge. Die Kaffeekasse berechnet den Betrag sofort mit dem hinterlegten Preis.

-
- Export & Korrekturen -

- -

-
    -
  • Download-Link fuer Buchungs-Export: Platzhalter fuer spaetere CSV- oder Monatsansicht
  • -
  • Storno- und Korrekturhinweise: Platzhalter fuer kuenftige Bedienhilfe direkt am Workflow
  • -
  • -
+
+ Buchungen als CSV exportieren
- + + +

Buchung für

+ + + + - - -

Wenn du mehrere Namen siehst, nutze im Pilot bitte nur deinen eigenen Eintrag. Falsche Buchungen korrigiert das Standortteam.

- +
- Pilot-Hinweis + Hinweise
  • - Eigene Historie - Ein persoenlicher Verlauf wird spaeter hier eingeblendet. + Ihr Verlauf + Ihre Buchungen können Sie als Tabelle exportieren.
  • Korrekturen - Falsche Buchungen bitte direkt an das Standortteam melden. -
  • -
  • - Exporte - Downloads und Monatsauszuege folgen nach weiterem Pilotfeedback. + Falsche Buchungen bitte direkt an die verantwortliche Person melden.
@@ -105,22 +100,23 @@ require __DIR__ . '/../partials/layout-top.php';
- + - + + - + diff --git a/resources/views/tenant/dashboard.php b/resources/views/tenant/dashboard.php index f0df0af..6b15297 100644 --- a/resources/views/tenant/dashboard.php +++ b/resources/views/tenant/dashboard.php @@ -1,17 +1,23 @@ 'Selbst gebucht', + 'paper_sheet' => 'Papierliste', + 'admin_backoffice' => 'Nachgetragen durch Verwaltung', + 'rfid_reader' => 'Kartenleser', +]; require __DIR__ . '/../partials/layout-top.php'; ?>
- +

-

Rolle: . Dieser Zugang ist fuer die taegliche Selbstbuchung gedacht; Stammdaten, Saldenpflege und Korrekturen bleiben beim Standortteam.

+

Buchen Sie hier Ihren Kaffee oder andere Produkte und sehen Sie Ihren aktuellen offenen Betrag. Korrekturen übernimmt die verantwortliche Person.

-

Rolle: . Hier laufen digitale Buchungen, Backoffice-Korrekturen und Standortzahlen zusammen.

+

Hier sehen Sie, wer Kaffee gebucht hat, welche Beträge offen sind und was als Nächstes in der Kaffeekasse gepflegt werden muss.

@@ -20,31 +26,67 @@ require __DIR__ . '/../partials/layout-top.php';
+
+ + + + + + + Verlauf exportieren + Eigene Buchungen als Tabelle herunterladen + + + + Mitglied anlegen + Person für Buchung, Papierliste oder Login hinzufügen + + + Geld einzahlen + Barzahlung, Überweisung oder Sammelkasse zuordnen + + + Produkte verwalten + Kaffee, Getränke und Preise pflegen + + +
+ +
+
+ Produkte + +
+
+ Offener Betrag + +
+
+
- Was du hier tun kannst + Ihr Kaffeekonto
    -
  • Digitale Buchungen fuer deinen Standort erfassen
  • -
  • Produktpreise vor dem Buchen einsehen
  • -
  • Passwort-Reset oder Buchungskorrekturen ueber das Standortteam anstossen
  • +
  • Kaffee, Getränke oder Snacks auf Ihren Namen buchen
  • +
  • Preise vor dem Buchen einsehen
  • +
  • Bei Fragen die verantwortliche Person informieren
-

Die Navigation zeigt hier bewusst nur Self-Service-Seiten, damit dieser Zugang nicht wie Volladministration wirkt.

- Pilotbetrieb + Kaffeekasse
  • Stammdaten und Mitglieder - Pflegt weiterhin das Standortteam im Backoffice. + Pflegen die verantwortlichen Personen der Kaffeeliste.
  • Einzahlungen und Papierlisten - Werden separat verbucht und bei Bedarf dort korrigiert. + Werden von den verantwortlichen Personen verbucht und bei Bedarf korrigiert.
  • - Exporte und persoenliche Historie - Werden im Pilot noch schrittweise nachgezogen. + Eigener Verlauf + Kann über den Export heruntergeladen werden.
@@ -64,7 +106,7 @@ require __DIR__ . '/../partials/layout-top.php';
- Offene Salden + Offene Beträge
@@ -76,25 +118,26 @@ require __DIR__ . '/../partials/layout-top.php';
  • Noch keine Buchungen - Die letzten Ereignisse erscheinen hier, sobald der Pilotstandort aktiv bucht. + Die letzten Buchungen erscheinen hier, sobald die erste Kaffeebuchung erfasst wurde.
  • +
  • - via + , gebucht über am
  • - Hohe Salden + Offene Beträge
    • - Noch keine Salden - Sobald Buchungen oder Einzahlungen vorliegen, erscheint hier die Uebersicht. + Noch keine offenen Beträge + Sobald Buchungen oder Einzahlungen vorliegen, erscheint hier die Übersicht.
    • diff --git a/resources/views/tenant/login.php b/resources/views/tenant/login.php index 1d230c0..8146b5b 100644 --- a/resources/views/tenant/login.php +++ b/resources/views/tenant/login.php @@ -5,34 +5,34 @@ require __DIR__ . '/../partials/layout-top.php'; ?>
      - Standortzugang + Kaffeeliste

      -

      Login fuer Owner, Standortteam und Mitglieder-Self-Service im Pilotbetrieb. Verwaltungsseiten bleiben bewusst dem Standortteam vorbehalten.

      +

      Melden Sie sich an, um Kaffee zu buchen, den eigenen Verlauf zu sehen oder die Kaffeeliste zu verwalten.

      - -

      Passwort vergessen? Reset anfragen

      + +

      Passwort vergessen? Passwort zurücksetzen

      diff --git a/resources/views/tenant/members.php b/resources/views/tenant/members.php index 14d94f8..7e1e69d 100644 --- a/resources/views/tenant/members.php +++ b/resources/views/tenant/members.php @@ -1,42 +1,32 @@
      - Standortteam -

      Mitglieder und Zugaenge

      -

      Mitglieder koennen rein fachlich gefuehrt werden oder optional einen Login fuer die Selbstbuchung erhalten. PINs werden nach dem Speichern nicht mehr im Klartext angezeigt.

      -
      - -
      - Export & Korrekturen -

      Hier ist bewusst Platz fuer spaetere CSV-Exporte, Sammelimporte und Statuskorrekturen reserviert, ohne dafuer heute schon neue Controllerlogik zu verlangen.

      -
        -
      • Mitgliederliste als Export: Platzhalter fuer kuenftigen Download-Link
      • -
      • Login-, Rollen- oder PIN-Korrekturen: Platzhalter fuer Hinweise direkt am Datensatz
      • -
      • Self-Service-Logins bleiben getrennt von den Backoffice-Feldern des Standortteams
      • -
      + Kaffeeliste +

      Mitglieder der Kaffeeliste

      +

      Legen Sie alle Personen an, die Kaffee buchen oder auf Papierlisten stehen. Ein Login ist nur nötig, wenn Mitglieder selbst buchen sollen. PINs werden nach dem Speichern nicht mehr im Klartext angezeigt.

      - -

      Wenn eine Login-E-Mail gesetzt ist, wird automatisch ein Member-Login fuer die digitale Selbstbuchung angelegt.

      + +

      Ohne E-Mail bleibt das Mitglied ein Listenprofil für Nachträge und Papierlisten.

      @@ -44,12 +34,17 @@
    - - + + + + + + + @@ -60,7 +55,7 @@ aktiv
    - nur Listenprofil + nur Verwaltung oder Papierliste - + - + @@ -134,7 +127,7 @@
    MandantKaffeeliste Paket Status Mitglieder Tenant oeffnenKaffeeliste öffnen
    Zeit Mitglied ProduktQuelleBuchungsart Betrag
    Noch keine Buchungen erfasst.Noch keine Kaffee-Buchungen. Die erste Buchung können Sie links erfassen.
    NameLoginPIN-StatusSelbstbuchungKiosk-PIN Status
    Noch keine Mitglieder. Legen Sie das erste Mitglied links im Formular an.
    diff --git a/resources/views/tenant/paper-sheets.php b/resources/views/tenant/paper-sheets.php index e39d047..76f9d4b 100644 --- a/resources/views/tenant/paper-sheets.php +++ b/resources/views/tenant/paper-sheets.php @@ -1,30 +1,22 @@
    - Backoffice -

    Papierlisten und Nacherfassung

    -

    Die klassische Kaffeeliste bleibt erhalten, wird aber als geordneter Beleg mit sauberem Posting ins Ledger behandelt. Dieser Bereich ist bewusst fuer das Standortteam formuliert, nicht fuer Member-Self-Service.

    -
    - -
    - Export & Korrekturen -
      -
    • Druck- oder CSV-Export fuer Papierlisten: Platzhalter fuer spaetere Links
    • -
    • Rueckfragen zu einzelnen Positionen: Platzhalter fuer kuenftige Korrekturhinweise am Entwurf
    • -
    • Member-Self-Service bleibt von Entwurf, Posting und Nachbearbeitung getrennt
    • -
    + Papierlisten +

    Papierliste nachtragen

    +

    Übertragen Sie Strichlisten oder Thekenzettel in die Kaffeekasse. Erst beim Abschließen werden die Positionen als Buchungen gezählt.

    +

    Neue Papierliste

    Titel StatusZeilenPositionen Zeitraum
    Noch keine Papierlisten angelegt.Noch keine Papierlisten. Legen Sie zuerst die erste Liste an.
    - + @@ -143,7 +136,7 @@ - + diff --git a/resources/views/tenant/payments.php b/resources/views/tenant/payments.php index 476ebc2..d945149 100644 --- a/resources/views/tenant/payments.php +++ b/resources/views/tenant/payments.php @@ -1,18 +1,9 @@
    - Backoffice -

    Einzahlungen verbuchen

    -

    Diese Seite bleibt bewusst beim Standortteam. Einzahlungen laufen ins selbe Ledger und reduzieren offene Salden nachvollziehbar, waehrend Mitglieder nur die Selbstbuchung sehen.

    -
    - -
    - Export & Korrekturen -
      -
    • Kassenexport oder Monatsabschluss: Platzhalter fuer kuenftige Download-Links
    • -
    • Rueckfragen zu Bareinzahlungen: Platzhalter fuer spaetere Korrekturhinweise direkt am Vorgang
    • -
    • Member-Self-Service blendet diesen Backoffice-Bereich bewusst aus
    • -
    + Kaffeekasse +

    Geld einzahlen

    +

    Tragen Sie Bargeld, Überweisungen oder Sammelzahlungen ein. Der offene Betrag des Mitglieds sinkt um diese Einzahlung.

    @@ -27,18 +18,18 @@ - +
    @@ -47,16 +38,16 @@
    - + - - - - - + + + + + diff --git a/resources/views/tenant/products.php b/resources/views/tenant/products.php index 43e12e5..fe2ce62 100644 --- a/resources/views/tenant/products.php +++ b/resources/views/tenant/products.php @@ -1,9 +1,9 @@
    - Sortiment + Kaffee und Produkte

    Produkte und Preise

    -

    Die Preislogik bleibt versionierbar. Jede Buchung friert den zum Zeitpunkt gueltigen Preis ein.

    +

    Legen Sie fest, was gebucht werden kann: Kaffee, Getränke, Snacks oder Pauschalen. Preisänderungen gelten nur für neue Buchungen.

    @@ -25,12 +25,17 @@
    - + + + + + + diff --git a/resources/views/tenant/rfid.php b/resources/views/tenant/rfid.php index 6824ac8..dd86082 100644 --- a/resources/views/tenant/rfid.php +++ b/resources/views/tenant/rfid.php @@ -1,18 +1,9 @@
    - Pilot & Integration -

    RFID-Roadmap und Event-Inbox

    -

    Das MVP speichert Geraete, Karten und rohe Events bereits produktionsnah, ohne die spaetere Hardwareentscheidung vorwegzunehmen. Dieser Bereich bleibt klar beim Standortteam und der Pilotbetreuung.

    -
    - -
    - Export & Korrekturen -
      -
    • Event-Export oder Inbox-Download: Platzhalter fuer spaetere Support-Links
    • -
    • Karten- und Geraetekorrekturen: Platzhalter fuer Hinweise direkt an den Tabellen
    • -
    • Member-Self-Service blendet RFID-Setup bewusst aus, bis der Pilot weiter ist
    • -
    + RFID-Kaffeekasse +

    Kartenleser einrichten

    +

    Richten Sie Kartenleser ein und verbinden Sie Karten mit Mitgliedern. Eingelesene Karten erscheinen unten als Ereignisse.

    @@ -20,14 +11,14 @@ - +
    @@ -42,26 +33,26 @@ - +
    - API -

    POST `/api/rfid/intake` mit JSON-Body:

    + Schnittstelle für den Leser +

    Der Leser sendet Ereignisse in diesem Format:

    {
       "device_token": "...",
       "uid": "04AABBCCDD",
       "event_id": "reader-0001",
       "ts": "2026-06-15T09:30:00Z"
     }
    -

    Events werden tenant-scharf gespeichert und spaeter in Buchungsregeln ueberfuehrt.

    +

    Ein Ereignis ist noch keine Kaffeebuchung. Es zeigt zuerst, welche Karte an welchem Leser erkannt wurde.

    @@ -70,16 +61,16 @@
    PapierlisteListe Mitglied Produkt Menge
    Noch keine Positionen erfasst.Noch keine Striche oder Positionen übertragen.
    Zeit MitgliedBeschreibungZahlungsnotiz Betrag
    Noch keine Einzahlungen verbucht.
    Noch keine Einzahlungen erfasst. Tragen Sie links die erste Zahlung in die Kaffeekasse ein.
    ProduktSKUKurzkennung Preis Status
    Noch keine Produkte. Legen Sie zuerst z. B. Kaffee, Espresso oder Tee an.
    - + - + - + @@ -106,7 +97,7 @@ - + @@ -127,15 +118,15 @@ - + - + - + diff --git a/storage/test.sqlite b/storage/test.sqlite index 5889c45..a54fd13 100644 Binary files a/storage/test.sqlite and b/storage/test.sqlite differ
    GeraetKartenleser Standort StatusTokenGeräte-Token
    Noch keine RFID-Geraete vorbereitet.Noch kein Kartenleser angelegt. Legen Sie oben den ersten Leser an.
    Noch keine RFID-Karten zugewiesen.Noch keine Karten mit Mitgliedern verbunden.
    ZeitGeraetKartenleser StatusEvent-IDEreignis-ID
    Noch keine RFID-Events empfangen.Noch keine Karte wurde von einem Leser gemeldet.