design anpassung
This commit is contained in:
+3
-3
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'] ?? ''));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => $_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' => [
|
||||
|
||||
+362
-143
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
<section class="page-head inline-head">
|
||||
<div>
|
||||
<span class="eyebrow">Betrieb</span>
|
||||
<h1>Plattform-Dashboard</h1>
|
||||
<p>Ueberblick ueber Mandanten, Paketstand und direkte Einstiege in die Kundenumgebungen.</p>
|
||||
<span class="eyebrow">Plattform</span>
|
||||
<h1>Kaffeelisten verwalten</h1>
|
||||
<p>Überblick über Kaffeelisten, Pakete und direkte Einstiege in die jeweiligen Arbeitsbereiche.</p>
|
||||
</div>
|
||||
<form method="post" action="<?= e(url('/admin/logout')) ?>">
|
||||
<?= csrf_field($csrf) ?>
|
||||
@@ -16,7 +16,7 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mandant</th>
|
||||
<th>Kaffeeliste</th>
|
||||
<th>Paket</th>
|
||||
<th>Status</th>
|
||||
<th>Mitglieder</th>
|
||||
@@ -32,10 +32,10 @@
|
||||
<small>/t/<?= e($tenant['slug']) ?></small>
|
||||
</td>
|
||||
<td><?= e(ucfirst($tenant['plan'])) ?></td>
|
||||
<td><?= e($tenant['status']) ?></td>
|
||||
<td><span class="status-pill"><?= e($tenant['status']) ?></span></td>
|
||||
<td><?= e((string) $tenant['member_count']) ?></td>
|
||||
<td><?= e((string) $tenant['product_count']) ?></td>
|
||||
<td><a class="text-link" href="<?= e(tenant_url($tenant['slug'])) ?>">Tenant oeffnen</a></td>
|
||||
<td><a class="text-link" href="<?= e(tenant_url($tenant['slug'])) ?>">Kaffeeliste öffnen</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
||||
@@ -5,33 +5,33 @@ require __DIR__ . '/../partials/layout-top.php';
|
||||
|
||||
<section class="page-head">
|
||||
<span class="eyebrow">Plattformzugang</span>
|
||||
<h1>Admin-Login</h1>
|
||||
<p>Fuer Pilotbetreuung, Mandantenuebersicht und Betriebsaufgaben. Mitglieder nutzen dafuer nicht diese Seite, sondern den jeweiligen Standort-Login.</p>
|
||||
<h1>Kaffeelisten verwalten</h1>
|
||||
<p>Dieser Bereich ist für Einrichtung, Standorte und Betriebsaufgaben vorgesehen.</p>
|
||||
</section>
|
||||
|
||||
<div class="two-column">
|
||||
<form class="card form-card" method="post" action="<?= e(url('/admin/login')) ?>">
|
||||
<?= csrf_field($csrf) ?>
|
||||
<label>
|
||||
<span>E-Mail</span>
|
||||
<span>E-Mail-Adresse</span>
|
||||
<input type="email" name="email" value="<?= e((string) old('email')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Passwort</span>
|
||||
<input type="password" name="password" required>
|
||||
</label>
|
||||
<button class="button button-primary" type="submit">Plattform anmelden</button>
|
||||
<p class="muted">Passwort vergessen? <a class="text-link" href="<?= e($resetRequestUrl) ?>">Reset anstossen</a></p>
|
||||
<button class="button button-primary" type="submit">Anmelden</button>
|
||||
<p class="muted">Passwort vergessen? <a class="text-link" href="<?= e($resetRequestUrl) ?>">Passwort zurücksetzen</a></p>
|
||||
</form>
|
||||
|
||||
<aside class="card" id="reset-help">
|
||||
<span class="eyebrow">Passwort vergessen</span>
|
||||
<h2>Reset im Pilotbetrieb</h2>
|
||||
<p class="muted">Der Passwort-Reset ist fuer den Pilot bereits vorgesehen. Bis die automatische Zustellung freigeschaltet ist, sollte das Plattformteam die Anfrage pruefen und den Reset manuell begleiten.</p>
|
||||
<h2>Passwort wiederherstellen</h2>
|
||||
<p class="muted">Fordern Sie einen Link für die hinterlegte E-Mail-Adresse an. Mitglieder nutzen den Login ihrer jeweiligen Kaffeeliste.</p>
|
||||
<ul class="feature-list">
|
||||
<li>Nur fuer Plattform-Admins und Pilotbetreuung</li>
|
||||
<li>Anfrage immer ueber die bekannte Login-E-Mail pruefen</li>
|
||||
<li>Mandanten- und Member-Logins laufen getrennt vom Plattformzugang</li>
|
||||
<li>Nur für Administratorinnen und Administratoren</li>
|
||||
<li>Anfragen laufen über die bekannte Login-E-Mail</li>
|
||||
<li>Kaffeelisten-Zugang und Plattformverwaltung bleiben getrennt</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -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';
|
||||
?>
|
||||
|
||||
<section class="page-head">
|
||||
<span class="eyebrow">Reset bestaetigen</span>
|
||||
<span class="eyebrow">Passwort erneuern</span>
|
||||
<h1>Neues Passwort setzen</h1>
|
||||
<p>
|
||||
<?= e($hasValidResetToken
|
||||
? ($scope === 'platform'
|
||||
? 'Der Plattformzugang bekommt hier sein neues Passwort.'
|
||||
: 'Der Standortzugang fuer ' . $tenantName . ' bekommt hier sein neues Passwort.')
|
||||
: 'Hier wird nur mit einem gueltigen Reset-Link ein neues Passwort gesetzt.') ?>
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -75,9 +75,9 @@ require __DIR__ . '/../partials/layout-top.php';
|
||||
<input type="hidden" name="token" value="<?= e($token) ?>">
|
||||
<?php if ($resetEmail !== '' || $expiresAt !== ''): ?>
|
||||
<p class="muted">
|
||||
<?= e($resetEmail !== '' ? 'Link fuer ' . $resetEmail . '.' : 'Der Reset-Link wurde geprueft.') ?>
|
||||
<?= e($resetEmail !== '' ? 'Link für ' . $resetEmail . '.' : 'Der Link wurde geprüft.') ?>
|
||||
<?php if ($expiresAt !== ''): ?>
|
||||
Gueltig bis <?= e($expiresAt) ?> UTC.
|
||||
Gültig bis <?= e($expiresAt) ?>.
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
@@ -89,29 +89,29 @@ require __DIR__ . '/../partials/layout-top.php';
|
||||
<span>Passwort wiederholen</span>
|
||||
<input type="password" name="password_confirm" minlength="12" required>
|
||||
</label>
|
||||
<button class="button button-primary" type="submit">Passwort uebernehmen</button>
|
||||
<p class="muted">Nach erfolgreicher Bestaetigung soll dieser Flow wieder in den passenden Login zurueckfuehren.</p>
|
||||
<p><a class="text-link" href="<?= e($loginUrl) ?>">Zurueck zum Login</a></p>
|
||||
<button class="button button-primary" type="submit">Passwort speichern</button>
|
||||
<p class="muted">Danach geht es zurück zur Anmeldung.</p>
|
||||
<p><a class="text-link" href="<?= e($loginUrl) ?>">Zurück zum Login</a></p>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<div class="card form-card">
|
||||
<h2>Reset-Link neu anfordern</h2>
|
||||
<h2>Neuen Link anfordern</h2>
|
||||
<p class="muted">
|
||||
<?= e($token === ''
|
||||
? 'Oeffne den Reset-Link aus der E-Mail, um hier ein neues Passwort zu setzen.'
|
||||
: 'Dieser Reset-Link ist ungueltig oder abgelaufen. Bitte fordere einen neuen Link an.') ?>
|
||||
? 'Ö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.') ?>
|
||||
</p>
|
||||
<p><a class="text-link" href="<?= e($requestUrl) ?>">Neuen Reset-Link anfordern</a></p>
|
||||
<p><a class="text-link" href="<?= e($loginUrl) ?>">Zurueck zum Login</a></p>
|
||||
<p><a class="text-link" href="<?= e($requestUrl) ?>">Neuen Link anfordern</a></p>
|
||||
<p><a class="text-link" href="<?= e($loginUrl) ?>">Zurück zum Login</a></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<aside class="card">
|
||||
<span class="eyebrow">Worauf geachtet wird</span>
|
||||
<ul class="feature-list">
|
||||
<li>Mindestens 12 Zeichen fuer Plattform- und Standortzugang</li>
|
||||
<li>Das neue Passwort ersetzt nur den Login, nicht PIN oder RFID-Zuweisungen</li>
|
||||
<li>Bei Rueckfragen bleibt das Standortteam im Pilot die erste Anlaufstelle</li>
|
||||
<li>Mindestens 12 Zeichen für Plattform- und Kaffeelisten-Zugang</li>
|
||||
<li>Das neue Passwort ersetzt nur den Login, nicht PIN oder Karten</li>
|
||||
<li>Bei Rückfragen hilft die verantwortliche Person der Kaffeeliste</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -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';
|
||||
?>
|
||||
|
||||
<section class="page-head">
|
||||
<span class="eyebrow">Passwort vergessen</span>
|
||||
<h1>Reset-Anfrage vorbereiten</h1>
|
||||
<h1>Neues Passwort anfordern</h1>
|
||||
<p>
|
||||
<?= e($scope === 'platform'
|
||||
? 'Diese Seite sammelt die Anfrage fuer einen Plattform-Reset.'
|
||||
: 'Diese Seite sammelt die Anfrage fuer den Standort-Reset von ' . $tenantName . '.') ?>
|
||||
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.') ?>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -61,29 +60,29 @@ require __DIR__ . '/../partials/layout-top.php';
|
||||
</label>
|
||||
<?php if ($scope !== 'platform' && $tenantSlug === ''): ?>
|
||||
<label>
|
||||
<span>Standort-Slug</span>
|
||||
<input type="text" name="tenant_slug" value="<?= e((string) old('tenant_slug')) ?>" placeholder="mein-team" required>
|
||||
<span>Kurzname der Kaffeeliste</span>
|
||||
<input type="text" name="tenant_slug" value="<?= e((string) old('tenant_slug')) ?>" placeholder="kaffeeliste-nord" required>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
<button class="button button-primary" type="submit">Reset-Link anfordern</button>
|
||||
<p class="muted">Nach dem Versand soll der Link wieder auf diese Strecke zurueckfuehren und danach zum passenden Login.</p>
|
||||
<p><a class="text-link" href="<?= e($loginUrl) ?>">Zurueck zum Login</a></p>
|
||||
<button class="button button-primary" type="submit">Link per E-Mail senden</button>
|
||||
<p class="muted">Nach dem Klick auf den Link können Sie ein neues Passwort festlegen.</p>
|
||||
<p><a class="text-link" href="<?= e($loginUrl) ?>">Zurück zum Login</a></p>
|
||||
</form>
|
||||
|
||||
<aside class="card">
|
||||
<span class="eyebrow">Pilotablauf</span>
|
||||
<span class="eyebrow">So geht es weiter</span>
|
||||
<ul class="stack-list">
|
||||
<li>
|
||||
<strong>1. E-Mail pruefen</strong>
|
||||
<small>Die Anfrage sollte immer ueber die bekannte Login-Adresse laufen.</small>
|
||||
<strong>1. E-Mail-Adresse eingeben</strong>
|
||||
<small>Nutzen Sie die Adresse, mit der der Zugang angelegt wurde.</small>
|
||||
</li>
|
||||
<li>
|
||||
<strong>2. Reset bestaetigen</strong>
|
||||
<small>Im Pilot kann diese Freigabe vorerst noch manuell durch Plattform- oder Standortteam begleitet werden.</small>
|
||||
<strong>2. Link öffnen</strong>
|
||||
<small>Der Link ist zeitlich begrenzt und nur einmal nutzbar.</small>
|
||||
</li>
|
||||
<li>
|
||||
<strong>3. Neues Passwort setzen</strong>
|
||||
<small>Danach geht es ueber die Confirm-Seite direkt zurueck in den passenden Login.</small>
|
||||
<small>Danach melden Sie sich mit dem neuen Passwort an.</small>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
@@ -2,37 +2,37 @@
|
||||
|
||||
<section class="hero">
|
||||
<div class="hero-copy">
|
||||
<span class="eyebrow">Pilotbetrieb fuer Teams und Vereine</span>
|
||||
<h1>Die digitale Kaffeeliste ist bereit fuer echte Pilotstandorte, nicht fuer leere Demo-Versprechen.</h1>
|
||||
<p class="lead">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.</p>
|
||||
<span class="eyebrow">Digitale Kaffeeliste für Teams, Vereine und Standorte</span>
|
||||
<h1>Kaffeekasse verwalten, ohne Zettel und Tabellen auseinanderzuhalten.</h1>
|
||||
<p class="lead">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.</p>
|
||||
<div class="button-row">
|
||||
<?php if ($setupComplete): ?>
|
||||
<a class="button button-primary" href="<?= e(url('/register')) ?>">Pilotmandant anlegen</a>
|
||||
<a class="button button-primary" href="<?= e(url('/register')) ?>">Kaffeeliste anlegen</a>
|
||||
<a class="button button-secondary" href="<?= e(url('/admin/login')) ?>">Plattform-Login</a>
|
||||
<?php else: ?>
|
||||
<a class="button button-primary" href="<?= e(url('/install')) ?>">Installation starten</a>
|
||||
<a class="button button-primary" href="<?= e(url('/install')) ?>">Kaffeeliste installieren</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<ul class="metric-row">
|
||||
<li><strong>1</strong><span>gemeinsames Ledger fuer Selbstbuchung, Papier und spaetere RFID-Events</span></li>
|
||||
<li><strong>Pilot</strong><span>klare Trennung zwischen Member-Self-Service und Standort-Backoffice</span></li>
|
||||
<li><strong>100%</strong><span>PHP/MySQL-Betrieb fuer klassisches Webhosting</span></li>
|
||||
<li><strong>Digital</strong><span>Kaffee direkt selbst oder durch die Verwaltung buchen</span></li>
|
||||
<li><strong>Papier</strong><span>Strichlisten nachtragen und gesammelt abschließen</span></li>
|
||||
<li><strong>Saldo</strong><span>Offene Beträge pro Person nachvollziehen</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<aside class="hero-card">
|
||||
<h2>Worauf der Pilot gerade abzielt</h2>
|
||||
<h2>Was die Kaffeeliste abnimmt</h2>
|
||||
<ul class="feature-list">
|
||||
<li>Owner-Onboarding mit sofort nutzbarem Testmandanten</li>
|
||||
<li>Mitglieder-Logins fuer schlanke Selbstbuchung statt Volladmin-Anmutung</li>
|
||||
<li>Papierlisten als Entwurf, Nacherfassung und sauberes Ledger-Posting</li>
|
||||
<li>Einzahlungen, Salden und Audit-Trail fuer das Standortteam</li>
|
||||
<li>RFID-Devices, Karten und Event-Inbox fuer spaetere Automatisierung</li>
|
||||
<li>Kaffeeliste mit verantwortlicher Person einrichten</li>
|
||||
<li>Mitglieder mit oder ohne Login anlegen</li>
|
||||
<li>Produkte, Preise und Einzahlungen nachvollziehbar pflegen</li>
|
||||
<li>Papierlisten als Entwurf erfassen und gesammelt übernehmen</li>
|
||||
<li>Kartenleser und Karten vorbereiten, wenn später RFID dazukommt</li>
|
||||
</ul>
|
||||
<?php if (is_array($stats)): ?>
|
||||
<div class="stats-panel">
|
||||
<div><strong><?= e((string) $stats['tenants']) ?></strong><span>Pilotmandanten in der Datenbank</span></div>
|
||||
<div><strong><?= e((string) $stats['tenants']) ?></strong><span>Kaffeelisten in der Datenbank</span></div>
|
||||
<div><strong><?= e((string) $stats['members']) ?></strong><span>Mitglieder</span></div>
|
||||
<div><strong><?= e((string) $stats['events']) ?></strong><span>Buchungsvorfaelle</span></div>
|
||||
<div><strong><?= e((string) $stats['events']) ?></strong><span>Buchungen</span></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</aside>
|
||||
@@ -40,53 +40,53 @@
|
||||
|
||||
<section class="content-grid">
|
||||
<article class="card">
|
||||
<span class="eyebrow">Heute stabil</span>
|
||||
<h2>Eine Buchungslogik fuer reale Nutzung</h2>
|
||||
<p>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.</p>
|
||||
<span class="eyebrow">Buchungen</span>
|
||||
<h2>Aus jedem Strich wird ein nachvollziehbarer Vorgang</h2>
|
||||
<p>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.</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="eyebrow">Pilotfokus</span>
|
||||
<h2>Self-Service fuer Mitglieder, Backoffice fuers Standortteam</h2>
|
||||
<p>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.</p>
|
||||
<span class="eyebrow">Verantwortung</span>
|
||||
<h2>Mitglieder buchen selbst, die Kaffeekasse bleibt geordnet</h2>
|
||||
<p>Mitglieder erfassen ihren Verbrauch schnell selbst. Verantwortliche pflegen Preise, buchen Einzahlungen, korrigieren Fehler und sehen offene Beträge pro Kaffeeliste.</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="eyebrow">Rahmen</span>
|
||||
<h2>Ehrlicher Rollout statt Schein-Automatisierung</h2>
|
||||
<p>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/`.</p>
|
||||
<span class="eyebrow">Papierliste</span>
|
||||
<h2>Papier bleibt möglich, die Abrechnung wird trotzdem sauber</h2>
|
||||
<p>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.</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="plans">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">Pilotstufen</span>
|
||||
<h2>Die Startkonfiguration im Pilot, nicht die finale Vermarktung</h2>
|
||||
<span class="eyebrow">Pakete</span>
|
||||
<h2>Vom digitalen Start bis zur erweiterten Kaffeekasse</h2>
|
||||
</div>
|
||||
<div class="plan-grid">
|
||||
<article class="plan-card">
|
||||
<h3>Starter</h3>
|
||||
<p>Fuer kleine Teams mit digitaler Buchung und moeglichst wenig Abstimmungsaufwand im Alltag.</p>
|
||||
<p>Für Teams, die ihre Kaffeekasse zuerst digital und ohne Zusatzhardware führen wollen.</p>
|
||||
<ul>
|
||||
<li>Mitglieder, Produkte und Selbstbuchung</li>
|
||||
<li>Schlanke Navigation fuer Member-Logins</li>
|
||||
<li>Schneller Einstieg fuer den ersten Pilotstandort</li>
|
||||
<li>Mitglieder, Produkte und Preise verwalten</li>
|
||||
<li>Buchungen und Einzahlungen digital erfassen</li>
|
||||
<li>Offene Beträge pro Person einsehen</li>
|
||||
</ul>
|
||||
</article>
|
||||
<article class="plan-card highlighted">
|
||||
<h3>Team</h3>
|
||||
<p>Fuer Organisationen, die Papierlisten und digitale Prozesse im Pilot parallel fahren.</p>
|
||||
<p>Für Organisationen, die digitale Buchungen und Papierlisten parallel nutzen.</p>
|
||||
<ul>
|
||||
<li>Papierlisten-Workflow mit Nacherfassung</li>
|
||||
<li>Backoffice fuer Einzahlungen und Korrekturen</li>
|
||||
<li>Vorbereitung fuer spaetere Export- und Supportpfade</li>
|
||||
<li>Papierlisten nacherfassen und gesammelt buchen</li>
|
||||
<li>Einzahlungen, Korrekturen und offene Beträge verwalten</li>
|
||||
<li>Buchungen als Tabelle exportieren</li>
|
||||
</ul>
|
||||
</article>
|
||||
<article class="plan-card">
|
||||
<h3>Business</h3>
|
||||
<p>Fuer Standorte mit RFID-Roadmap, hoeheren Betriebsanforderungen und fruehem Integrationsbedarf.</p>
|
||||
<p>Für Standorte, die später Kartenleser oder verbindlichere Abläufe ergänzen möchten.</p>
|
||||
<ul>
|
||||
<li>RFID-Devices, Karten und Intake-API</li>
|
||||
<li>Release-, Backup- und Deployment-Rahmen</li>
|
||||
<li>Frueher Blick auf Betriebs- und Supportprozesse</li>
|
||||
<li>Kartenleser vorbereiten und Karten zuweisen</li>
|
||||
<li>RFID-Ereignisse standortbezogen empfangen</li>
|
||||
<li>Betriebs-, Sicherungs- und Supportprozesse planen</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
@@ -94,15 +94,15 @@
|
||||
|
||||
<section class="timeline card">
|
||||
<div class="section-head">
|
||||
<span class="eyebrow">Pilotablauf</span>
|
||||
<h2>So startet ein neuer Standort realistisch</h2>
|
||||
<span class="eyebrow">Einrichtung</span>
|
||||
<h2>In wenigen Schritten zur nutzbaren Kaffeeliste</h2>
|
||||
</div>
|
||||
<ol class="step-list">
|
||||
<li>Plattform installieren und ersten Admin anlegen.</li>
|
||||
<li>Pilotmandant registrieren, Owner-Onboarding abschliessen und Produkte vorbelegen.</li>
|
||||
<li>Mitglieder fuer Selbstbuchung anlegen und Standort-Backoffice abstimmen.</li>
|
||||
<li>Digitale Buchungen und Papierlisten parallel im Team einsetzen.</li>
|
||||
<li>RFID-Geraete spaeter ueber die Event-Inbox anbinden, ohne das Kernledger umzubauen.</li>
|
||||
<li>Kaffeeliste mit verantwortlicher Person registrieren.</li>
|
||||
<li>Mitglieder, Produkte und Preise anlegen.</li>
|
||||
<li>Buchungen und Einzahlungen im Alltag erfassen.</li>
|
||||
<li>Papierlisten nachtragen oder Kartenleser später ergänzen.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
|
||||
|
||||
<section class="page-head">
|
||||
<span class="eyebrow">Pilot-Onboarding</span>
|
||||
<h1>Pilotmandant registrieren</h1>
|
||||
<p>Dieses Formular legt einen echten Teststandort an. Bitte nur verwenden, wenn Organisation, Ansprechpartner und Startpreis fuer den Pilotbetrieb abgestimmt sind.</p>
|
||||
<span class="eyebrow">Neue Kaffeeliste einrichten</span>
|
||||
<h1>Kaffeeliste registrieren</h1>
|
||||
<p>Legen Sie eine Kaffeeliste für Ihr Büro, Team oder Ihren Verein an. Danach können Sie Mitglieder, Produkte, Einzahlungen und Papierlisten verwalten.</p>
|
||||
</section>
|
||||
|
||||
<div class="two-column">
|
||||
<form class="card form-card" method="post" action="<?= e(url('/register')) ?>">
|
||||
<?= csrf_field($csrf) ?>
|
||||
<label>
|
||||
<span>Team- / Standortname</span>
|
||||
<span>Name der Kaffeeliste</span>
|
||||
<input type="text" name="tenant_name" value="<?= e((string) old('tenant_name')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Kurzname fuer die Login-URL</span>
|
||||
<input type="text" name="tenant_slug" value="<?= e((string) old('tenant_slug')) ?>" placeholder="mein-team">
|
||||
<span>Kurzname für die Webadresse</span>
|
||||
<input type="text" name="tenant_slug" value="<?= e((string) old('tenant_slug')) ?>" placeholder="kaffeeliste-nord">
|
||||
</label>
|
||||
<label>
|
||||
<span>Pilotstufe</span>
|
||||
<span>Paket</span>
|
||||
<select name="plan">
|
||||
<option value="starter">Starter</option>
|
||||
<option value="team" selected>Team</option>
|
||||
@@ -26,33 +26,33 @@
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Startpreis pro Kaffee in EUR</span>
|
||||
<span>Standardpreis pro Kaffee in EUR</span>
|
||||
<input type="number" name="default_price_eur" min="0.50" step="0.05" value="<?= e((string) old('default_price_eur', '1.20')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Ansprechpartner / Owner</span>
|
||||
<span>Verantwortliche Person</span>
|
||||
<input type="text" name="owner_name" value="<?= e((string) old('owner_name')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Login-E-Mail fuer den Owner</span>
|
||||
<span>E-Mail-Adresse für die Verwaltung</span>
|
||||
<input type="email" name="owner_email" value="<?= e((string) old('owner_email')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Startpasswort fuer den Owner</span>
|
||||
<span>Startpasswort</span>
|
||||
<input type="password" name="owner_password" minlength="12" required>
|
||||
</label>
|
||||
<button class="button button-primary" type="submit">Pilotmandant anlegen</button>
|
||||
<button class="button button-primary" type="submit">Kaffeeliste anlegen</button>
|
||||
</form>
|
||||
|
||||
<aside class="card">
|
||||
<span class="eyebrow">Vor dem Absenden</span>
|
||||
<span class="eyebrow">Hinweise</span>
|
||||
<ul class="feature-list">
|
||||
<li>Die Registrierung erzeugt sofort einen nutzbaren Testmandanten</li>
|
||||
<li>Owner-Zugang und erstes Mitglied werden automatisch angelegt</li>
|
||||
<li>Systemquellen fuer digital, Papier, Backoffice und RFID stehen direkt bereit</li>
|
||||
<li>Die Self-Service-Navigation fuer Mitglieder bleibt spaeter schlanker als das Admin-Backoffice</li>
|
||||
<li>Die Registrierung erzeugt sofort eine nutzbare Kaffeeliste</li>
|
||||
<li>Der erste Verwaltungszugang wird automatisch angelegt</li>
|
||||
<li>Mitglieder, Produkte, Einzahlungen und Papierlisten können danach ergänzt werden</li>
|
||||
<li>Passwörter müssen mindestens 12 Zeichen haben</li>
|
||||
</ul>
|
||||
<p class="muted">Wenn ihr erst unverbindlich pruefen wollt, startet lieber mit einem klar benannten Pilotteam statt mit Fantasiedaten. Das macht spaetere Login-, Reset- und Supportablaeufe deutlich sauberer.</p>
|
||||
<p class="muted">Ein klarer Name hilft später bei Login, Rückfragen und mehreren Kaffeelisten im selben Umfeld.</p>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<section class="page-head">
|
||||
<span class="eyebrow">Setup</span>
|
||||
<h1>Plattform installieren</h1>
|
||||
<p>Der Installer schreibt `.env`, legt das Datenbankschema an und erstellt den ersten Plattform-Admin. Danach ist das Self-Service-Onboarding fuer Mandanten freigeschaltet.</p>
|
||||
<p>Die Installation richtet Datenbank, Grundeinstellungen und den ersten Verwaltungszugang ein. Danach können Kaffeelisten eingerichtet werden.</p>
|
||||
|
||||
<?php
|
||||
$availableDrivers = PDO::getAvailableDrivers();
|
||||
@@ -73,7 +73,7 @@
|
||||
<input type="password" name="admin_password" minlength="12" required>
|
||||
</label>
|
||||
</div>
|
||||
<button class="button button-primary" type="submit">Installation abschliessen</button>
|
||||
<button class="button button-primary" type="submit">Installation abschließen</button>
|
||||
</form>
|
||||
|
||||
<?php require __DIR__ . '/../partials/layout-bottom.php'; ?>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<?php $footerBrandName = trim((string) ($config['name'] ?? '')) ?: 'Kaffeeliste'; ?>
|
||||
</main>
|
||||
<footer class="site-footer">
|
||||
<div class="shell footer-grid">
|
||||
<div>
|
||||
<strong>Kaffeekasse SaaS</strong>
|
||||
<p>Fuer Teams, Vereine und Standorte, die klassische Listen und digitale Selbstbuchung in einem System zusammenbringen wollen.</p>
|
||||
<strong><?= e($footerBrandName) ?></strong>
|
||||
<p>Für Teams, Vereine und Standorte, die Kaffee, Einzahlungen und offene Beträge an einem Ort im Griff behalten wollen.</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="eyebrow">Produktionsreif gedacht</span>
|
||||
<p>Path-basiertes Multi-Tenant-Setup, PHP/MySQL fuer Netcup-Webspace, Ledger-basierte Buchungen und vorbereitete RFID-Inbox.</p>
|
||||
<span class="eyebrow">Im Alltag nutzbar</span>
|
||||
<p>Mitglieder buchen selbst, Papierlisten lassen sich nachtragen und die verantwortliche Person behält die Kaffeekasse ohne Zettelchaos im Blick.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -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';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= e($title ?? $config['name']) ?></title>
|
||||
<title><?= e($title ?? $brandName) ?></title>
|
||||
<link rel="stylesheet" href="<?= e(asset_url('app.css')) ?>">
|
||||
</head>
|
||||
<body class="<?= e($pageClass ?? 'app-shell') ?>">
|
||||
@@ -24,40 +26,40 @@ $isTenantAdmin = in_array($tenantRole, ['owner', 'admin'], true);
|
||||
<header class="site-header">
|
||||
<div class="shell header-inner">
|
||||
<a class="brand" href="<?= e(url('/')) ?>">
|
||||
<span class="brand-mark">KK</span>
|
||||
<span class="brand-mark">KL</span>
|
||||
<span>
|
||||
<strong><?= e($config['name']) ?></strong>
|
||||
<small>Mandantenfaehige Kaffeekasse</small>
|
||||
<strong><?= e($brandName) ?></strong>
|
||||
<small>Kaffeeliste für Teams, Vereine und Büroküchen</small>
|
||||
</span>
|
||||
</a>
|
||||
<nav class="top-nav">
|
||||
<?php if ($hasTenantAccess): ?>
|
||||
<a href="<?= e(tenant_url($tenantSlug)) ?>" class="<?= $currentPath === tenant_path($tenantSlug) ? 'active' : '' ?>">Uebersicht</a>
|
||||
<a href="<?= e(tenant_url($tenantSlug, 'bookings')) ?>" class="<?= $currentPath === tenant_path($tenantSlug, 'bookings') ? 'active' : '' ?>">Buchen</a>
|
||||
<a href="<?= e(tenant_url($tenantSlug)) ?>" class="<?= $currentPath === tenant_path($tenantSlug) ? 'active' : '' ?>">Kaffeekasse</a>
|
||||
<a href="<?= e(tenant_url($tenantSlug, 'bookings')) ?>" class="<?= $currentPath === tenant_path($tenantSlug, 'bookings') ? 'active' : '' ?>">Kaffee buchen</a>
|
||||
<?php if ($isTenantAdmin): ?>
|
||||
<a href="<?= e(tenant_url($tenantSlug, 'members')) ?>" class="<?= $currentPath === tenant_path($tenantSlug, 'members') ? 'active' : '' ?>">Mitglieder</a>
|
||||
<a href="<?= e(tenant_url($tenantSlug, 'products')) ?>" class="<?= $currentPath === tenant_path($tenantSlug, 'products') ? 'active' : '' ?>">Produkte</a>
|
||||
<a href="<?= e(tenant_url($tenantSlug, 'payments')) ?>" class="<?= $currentPath === tenant_path($tenantSlug, 'payments') ? 'active' : '' ?>">Einzahlungen</a>
|
||||
<a href="<?= e(tenant_url($tenantSlug, 'paper-sheets')) ?>" class="<?= $currentPath === tenant_path($tenantSlug, 'paper-sheets') ? 'active' : '' ?>">Papierlisten</a>
|
||||
<a href="<?= e(tenant_url($tenantSlug, 'rfid')) ?>" class="<?= $currentPath === tenant_path($tenantSlug, 'rfid') ? 'active' : '' ?>">RFID</a>
|
||||
<a href="<?= e(tenant_url($tenantSlug, 'rfid')) ?>" class="<?= $currentPath === tenant_path($tenantSlug, 'rfid') ? 'active' : '' ?>">Kartenleser</a>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($tenantSlug !== ''): ?>
|
||||
<a href="<?= e(url('/')) ?>" class="<?= $currentPath === '/' ? 'active' : '' ?>">Start</a>
|
||||
<a href="<?= e(url('/register')) ?>" class="<?= $currentPath === '/register' ? 'active' : '' ?>">Pilot registrieren</a>
|
||||
<a href="<?= e(tenant_url($tenantSlug, 'login')) ?>" class="<?= $currentPath === tenant_path($tenantSlug, 'login') ? 'active' : '' ?>">Standort-Login</a>
|
||||
<a href="<?= e(url('/admin/login')) ?>" class="<?= str_starts_with($currentPath, '/admin') ? 'active' : '' ?>">Plattform</a>
|
||||
<a href="<?= e(url('/register')) ?>" class="<?= $currentPath === '/register' ? 'active' : '' ?>">Neue Kaffeeliste</a>
|
||||
<a href="<?= e(tenant_url($tenantSlug, 'login')) ?>" class="<?= $currentPath === tenant_path($tenantSlug, 'login') ? 'active' : '' ?>">Anmelden</a>
|
||||
<a href="<?= e(url('/admin/login')) ?>" class="<?= str_starts_with($currentPath, '/admin') ? 'active' : '' ?>">Plattform-Login</a>
|
||||
<?php else: ?>
|
||||
<a href="<?= e(url('/')) ?>" class="<?= $currentPath === '/' ? 'active' : '' ?>">Start</a>
|
||||
<a href="<?= e(url('/register')) ?>" class="<?= $currentPath === '/register' ? 'active' : '' ?>">Pilot registrieren</a>
|
||||
<a href="<?= e(url('/admin/login')) ?>" class="<?= str_starts_with($currentPath, '/admin') ? 'active' : '' ?>">Plattform</a>
|
||||
<a href="<?= e(url('/register')) ?>" class="<?= $currentPath === '/register' ? 'active' : '' ?>">Neue Kaffeeliste</a>
|
||||
<a href="<?= e(url('/admin/login')) ?>" class="<?= str_starts_with($currentPath, '/admin') ? 'active' : '' ?>">Plattform-Login</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main class="shell main-shell">
|
||||
<?php if ($flashSuccess): ?>
|
||||
<div class="flash flash-success"><?= e((string) $flashSuccess) ?></div>
|
||||
<div class="flash flash-success" role="status"><?= e((string) $flashSuccess) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($flashError): ?>
|
||||
<div class="flash flash-error"><?= e((string) $flashError) ?></div>
|
||||
<div class="flash flash-error" role="alert"><?= e((string) $flashError) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -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';
|
||||
?>
|
||||
|
||||
<section class="page-head">
|
||||
<span class="eyebrow"><?= e($isSelfService ? 'Selbstbuchung' : 'Digitale Erfassung') ?></span>
|
||||
<h1><?= e($isSelfService ? 'Getraenke buchen' : 'Digitale Buchungen') ?></h1>
|
||||
<span class="eyebrow"><?= e($isSelfService ? 'Mein Kaffee' : 'Kaffeekasse') ?></span>
|
||||
<h1>Kaffee buchen</h1>
|
||||
<?php if ($isSelfService): ?>
|
||||
<p>Diese Seite ist auf taegliche Member-Self-Service-Buchungen zugeschnitten. Korrekturen, Sammelbuchungen und Exporte bleiben im Pilot vorerst beim Standortteam.</p>
|
||||
<p>Wählen Sie, was Sie genommen haben. Die Buchung landet direkt auf Ihrem Kaffeekonto.</p>
|
||||
<?php else: ?>
|
||||
<p>Browser, Tablet und Kiosk landen im selben Ledger. Das Standortteam kann hier Selbstbuchung und Backoffice-Buchungen sichtbar trennen.</p>
|
||||
<p>Wählen Sie Mitglied, Produkt und Menge. Die Kaffeekasse berechnet den Betrag sofort mit dem hinterlegten Preis.</p>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<span class="eyebrow">Export & Korrekturen</span>
|
||||
<p class="muted">
|
||||
<?= e($isSelfService
|
||||
? 'Hier ist bewusst Platz fuer persoenliche Exporte und klare Korrekturhinweise reserviert, ohne heute schon neue Backend-Logik vorwegzunehmen.'
|
||||
: 'Hier ist bewusst Platz fuer CSV-Exporte, Sammelkorrekturen und Rueckfragen reserviert, ohne heute schon neue Controllerpfade vorwegzunehmen.') ?>
|
||||
</p>
|
||||
<ul class="feature-list">
|
||||
<li>Download-Link fuer Buchungs-Export: Platzhalter fuer spaetere CSV- oder Monatsansicht</li>
|
||||
<li>Storno- und Korrekturhinweise: Platzhalter fuer kuenftige Bedienhilfe direkt am Workflow</li>
|
||||
<li><?= e($isSelfService ? 'Bis dahin bitte im Pilot nur den eigenen Namen waehlen; persoenliche Vorauswahl folgt.' : 'Sammelkorrekturen und Nachbuchungen bleiben vorerst beim Standortteam.') ?></li>
|
||||
</ul>
|
||||
<section class="section-actions">
|
||||
<a class="button button-secondary" href="<?= e(tenant_url($tenant['slug'], 'bookings/export')) ?>">Buchungen als CSV exportieren</a>
|
||||
</section>
|
||||
|
||||
<div class="two-column">
|
||||
<form class="card form-card" method="post" action="<?= e(tenant_url($tenant['slug'], 'bookings')) ?>">
|
||||
<?= csrf_field($csrf) ?>
|
||||
<?php if ($isSelfService): ?>
|
||||
<?php if (is_array($currentMember)): ?>
|
||||
<p class="form-note">Buchung für <?= e($currentMember['display_name']) ?></p>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<label>
|
||||
<span><?= e($isSelfService ? 'Dein Mitgliedseintrag' : 'Mitglied') ?></span>
|
||||
<span>Mitglied</span>
|
||||
<select name="member_id" required>
|
||||
<?php foreach ($members as $member): ?>
|
||||
<option value="<?= e((string) $member['id']) ?>" <?= $selectedMemberId === (string) $member['id'] ? 'selected' : '' ?>><?= e($member['display_name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
<label>
|
||||
<span>Produkt</span>
|
||||
<select name="product_id" required>
|
||||
@@ -54,10 +56,10 @@ require __DIR__ . '/../partials/layout-top.php';
|
||||
<input type="hidden" name="source_code" value="digital_self">
|
||||
<?php else: ?>
|
||||
<label>
|
||||
<span>Quelle</span>
|
||||
<span>Buchungsart</span>
|
||||
<select name="source_code">
|
||||
<option value="digital_self" <?= $selectedSourceCode === 'digital_self' ? 'selected' : '' ?>>Digitale Selbstbuchung</option>
|
||||
<option value="admin_backoffice" <?= $selectedSourceCode === 'admin_backoffice' ? 'selected' : '' ?>>Backoffice-Buchung</option>
|
||||
<option value="digital_self" <?= $selectedSourceCode === 'digital_self' ? 'selected' : '' ?>>Selbst gebucht</option>
|
||||
<option value="admin_backoffice" <?= $selectedSourceCode === 'admin_backoffice' ? 'selected' : '' ?>>Nachgetragen durch Verwaltung</option>
|
||||
</select>
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
@@ -66,34 +68,27 @@ require __DIR__ . '/../partials/layout-top.php';
|
||||
<input type="number" name="quantity" min="0.25" step="0.25" value="<?= e((string) old('quantity', '1')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Verbrauchszeitpunkt</span>
|
||||
<span>Wann genommen?</span>
|
||||
<input type="datetime-local" name="effective_at" value="<?= e((string) old('effective_at', gmdate('Y-m-d\TH:i'))) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Notiz</span>
|
||||
<span>Notiz, falls nötig</span>
|
||||
<input type="text" name="notes" value="<?= e((string) old('notes')) ?>">
|
||||
</label>
|
||||
<button class="button button-primary" type="submit"><?= e($isSelfService ? 'Jetzt buchen' : 'Buchung speichern') ?></button>
|
||||
<?php if ($isSelfService): ?>
|
||||
<p class="muted">Wenn du mehrere Namen siehst, nutze im Pilot bitte nur deinen eigenen Eintrag. Falsche Buchungen korrigiert das Standortteam.</p>
|
||||
<?php endif; ?>
|
||||
<button class="button button-primary" type="submit">Auf Kaffeekonto buchen</button>
|
||||
</form>
|
||||
|
||||
<?php if ($isSelfService): ?>
|
||||
<section class="card">
|
||||
<span class="eyebrow">Pilot-Hinweis</span>
|
||||
<span class="eyebrow">Hinweise</span>
|
||||
<ul class="stack-list">
|
||||
<li>
|
||||
<strong>Eigene Historie</strong>
|
||||
<small>Ein persoenlicher Verlauf wird spaeter hier eingeblendet.</small>
|
||||
<strong>Ihr Verlauf</strong>
|
||||
<small>Ihre Buchungen können Sie als Tabelle exportieren.</small>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Korrekturen</strong>
|
||||
<small>Falsche Buchungen bitte direkt an das Standortteam melden.</small>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Exporte</strong>
|
||||
<small>Downloads und Monatsauszuege folgen nach weiterem Pilotfeedback.</small>
|
||||
<small>Falsche Buchungen bitte direkt an die verantwortliche Person melden.</small>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
@@ -105,22 +100,23 @@ require __DIR__ . '/../partials/layout-top.php';
|
||||
<th>Zeit</th>
|
||||
<th>Mitglied</th>
|
||||
<th>Produkt</th>
|
||||
<th>Quelle</th>
|
||||
<th>Buchungsart</th>
|
||||
<th>Betrag</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($bookings === []): ?>
|
||||
<tr>
|
||||
<td colspan="5">Noch keine Buchungen erfasst.</td>
|
||||
<td class="table-empty" colspan="5">Noch keine Kaffee-Buchungen. Die erste Buchung können Sie links erfassen.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($bookings as $booking): ?>
|
||||
<?php $sourceLabel = $sourceLabels[(string) ($booking['source_code'] ?? '')] ?? (string) $booking['source_name']; ?>
|
||||
<tr>
|
||||
<td><?= e($booking['recorded_at']) ?></td>
|
||||
<td><?= e($booking['member_name']) ?></td>
|
||||
<td><?= e($booking['product_name']) ?></td>
|
||||
<td><?= e($booking['source_name']) ?></td>
|
||||
<td><?= e($sourceLabel) ?></td>
|
||||
<td><?= e(money_from_cents((int) $booking['total_cents'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<?php
|
||||
$tenantRole = (string) ($authState['tenant_role'] ?? 'member');
|
||||
$isSelfService = $tenantRole === 'member';
|
||||
$sourceLabels = [
|
||||
'digital_self' => 'Selbst gebucht',
|
||||
'paper_sheet' => 'Papierliste',
|
||||
'admin_backoffice' => 'Nachgetragen durch Verwaltung',
|
||||
'rfid_reader' => 'Kartenleser',
|
||||
];
|
||||
require __DIR__ . '/../partials/layout-top.php';
|
||||
?>
|
||||
|
||||
<section class="page-head inline-head">
|
||||
<div>
|
||||
<span class="eyebrow"><?= e($isSelfService ? 'Member-Self-Service' : ucfirst((string) $tenant['plan']) . ' Paket') ?></span>
|
||||
<span class="eyebrow"><?= e($isSelfService ? 'Mein Kaffeekonto' : 'Kaffeekasse') ?></span>
|
||||
<h1><?= e($tenant['name']) ?></h1>
|
||||
<?php if ($isSelfService): ?>
|
||||
<p>Rolle: <?= e($tenantRole) ?>. Dieser Zugang ist fuer die taegliche Selbstbuchung gedacht; Stammdaten, Saldenpflege und Korrekturen bleiben beim Standortteam.</p>
|
||||
<p>Buchen Sie hier Ihren Kaffee oder andere Produkte und sehen Sie Ihren aktuellen offenen Betrag. Korrekturen übernimmt die verantwortliche Person.</p>
|
||||
<?php else: ?>
|
||||
<p>Rolle: <?= e($tenantRole) ?>. Hier laufen digitale Buchungen, Backoffice-Korrekturen und Standortzahlen zusammen.</p>
|
||||
<p>Hier sehen Sie, wer Kaffee gebucht hat, welche Beträge offen sind und was als Nächstes in der Kaffeekasse gepflegt werden muss.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<form method="post" action="<?= e(tenant_url($tenant['slug'], 'logout')) ?>">
|
||||
@@ -20,31 +26,67 @@ require __DIR__ . '/../partials/layout-top.php';
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="action-strip">
|
||||
<a class="quick-action primary" href="<?= e(tenant_url($tenant['slug'], 'bookings')) ?>">
|
||||
<strong><?= e($isSelfService ? 'Kaffee buchen' : 'Kaffee oder Produkt buchen') ?></strong>
|
||||
<span><?= e($isSelfService ? 'Kaffee, Getränke oder Snacks auf Ihren Namen buchen' : 'Verbrauch direkt einem Mitglied zuordnen') ?></span>
|
||||
</a>
|
||||
<?php if ($isSelfService): ?>
|
||||
<a class="quick-action" href="<?= e(tenant_url($tenant['slug'], 'bookings/export')) ?>">
|
||||
<strong>Verlauf exportieren</strong>
|
||||
<span>Eigene Buchungen als Tabelle herunterladen</span>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<a class="quick-action" href="<?= e(tenant_url($tenant['slug'], 'members')) ?>">
|
||||
<strong>Mitglied anlegen</strong>
|
||||
<span>Person für Buchung, Papierliste oder Login hinzufügen</span>
|
||||
</a>
|
||||
<a class="quick-action" href="<?= e(tenant_url($tenant['slug'], 'payments')) ?>">
|
||||
<strong>Geld einzahlen</strong>
|
||||
<span>Barzahlung, Überweisung oder Sammelkasse zuordnen</span>
|
||||
</a>
|
||||
<a class="quick-action" href="<?= e(tenant_url($tenant['slug'], 'products')) ?>">
|
||||
<strong>Produkte verwalten</strong>
|
||||
<span>Kaffee, Getränke und Preise pflegen</span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<?php if ($isSelfService): ?>
|
||||
<section class="metric-cards">
|
||||
<article class="metric-card">
|
||||
<span>Produkte</span>
|
||||
<strong><?= e((string) $summary['product_count']) ?></strong>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>Offener Betrag</span>
|
||||
<strong><?= e(money_from_cents((int) $summary['open_balance_cents'])) ?></strong>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<div class="two-column">
|
||||
<section class="card">
|
||||
<span class="eyebrow">Was du hier tun kannst</span>
|
||||
<span class="eyebrow">Ihr Kaffeekonto</span>
|
||||
<ul class="feature-list">
|
||||
<li>Digitale Buchungen fuer deinen Standort erfassen</li>
|
||||
<li>Produktpreise vor dem Buchen einsehen</li>
|
||||
<li>Passwort-Reset oder Buchungskorrekturen ueber das Standortteam anstossen</li>
|
||||
<li>Kaffee, Getränke oder Snacks auf Ihren Namen buchen</li>
|
||||
<li>Preise vor dem Buchen einsehen</li>
|
||||
<li>Bei Fragen die verantwortliche Person informieren</li>
|
||||
</ul>
|
||||
<p class="muted">Die Navigation zeigt hier bewusst nur Self-Service-Seiten, damit dieser Zugang nicht wie Volladministration wirkt.</p>
|
||||
</section>
|
||||
<section class="card">
|
||||
<span class="eyebrow">Pilotbetrieb</span>
|
||||
<span class="eyebrow">Kaffeekasse</span>
|
||||
<ul class="stack-list">
|
||||
<li>
|
||||
<strong>Stammdaten und Mitglieder</strong>
|
||||
<small>Pflegt weiterhin das Standortteam im Backoffice.</small>
|
||||
<small>Pflegen die verantwortlichen Personen der Kaffeeliste.</small>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Einzahlungen und Papierlisten</strong>
|
||||
<small>Werden separat verbucht und bei Bedarf dort korrigiert.</small>
|
||||
<small>Werden von den verantwortlichen Personen verbucht und bei Bedarf korrigiert.</small>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Exporte und persoenliche Historie</strong>
|
||||
<small>Werden im Pilot noch schrittweise nachgezogen.</small>
|
||||
<strong>Eigener Verlauf</strong>
|
||||
<small>Kann über den Export heruntergeladen werden.</small>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
@@ -64,7 +106,7 @@ require __DIR__ . '/../partials/layout-top.php';
|
||||
<strong><?= e((string) $summary['paper_sheet_count']) ?></strong>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span>Offene Salden</span>
|
||||
<span>Offene Beträge</span>
|
||||
<strong><?= e(money_from_cents((int) $summary['open_balance_cents'])) ?></strong>
|
||||
</article>
|
||||
</section>
|
||||
@@ -76,25 +118,26 @@ require __DIR__ . '/../partials/layout-top.php';
|
||||
<?php if ($latestEvents === []): ?>
|
||||
<li>
|
||||
<strong>Noch keine Buchungen</strong>
|
||||
<small>Die letzten Ereignisse erscheinen hier, sobald der Pilotstandort aktiv bucht.</small>
|
||||
<small>Die letzten Buchungen erscheinen hier, sobald die erste Kaffeebuchung erfasst wurde.</small>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($latestEvents as $event): ?>
|
||||
<?php $sourceLabel = $sourceLabels[(string) ($event['source_code'] ?? '')] ?? (string) $event['source_name']; ?>
|
||||
<li>
|
||||
<strong><?= e($event['member_name']) ?></strong>
|
||||
<span><?= e($event['product_name']) ?> via <?= e($event['source_name']) ?></span>
|
||||
<span><?= e($event['product_name']) ?>, gebucht über <?= e($sourceLabel) ?></span>
|
||||
<small><?= e(money_from_cents((int) $event['total_cents'])) ?> am <?= e($event['recorded_at']) ?></small>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="card">
|
||||
<span class="eyebrow">Hohe Salden</span>
|
||||
<span class="eyebrow">Offene Beträge</span>
|
||||
<ul class="stack-list">
|
||||
<?php if ($memberBalances === []): ?>
|
||||
<li>
|
||||
<strong>Noch keine Salden</strong>
|
||||
<small>Sobald Buchungen oder Einzahlungen vorliegen, erscheint hier die Uebersicht.</small>
|
||||
<strong>Noch keine offenen Beträge</strong>
|
||||
<small>Sobald Buchungen oder Einzahlungen vorliegen, erscheint hier die Übersicht.</small>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($memberBalances as $balance): ?>
|
||||
|
||||
@@ -5,34 +5,34 @@ require __DIR__ . '/../partials/layout-top.php';
|
||||
?>
|
||||
|
||||
<section class="page-head">
|
||||
<span class="eyebrow">Standortzugang</span>
|
||||
<span class="eyebrow">Kaffeeliste</span>
|
||||
<h1><?= e($tenant['name']) ?></h1>
|
||||
<p>Login fuer Owner, Standortteam und Mitglieder-Self-Service im Pilotbetrieb. Verwaltungsseiten bleiben bewusst dem Standortteam vorbehalten.</p>
|
||||
<p>Melden Sie sich an, um Kaffee zu buchen, den eigenen Verlauf zu sehen oder die Kaffeeliste zu verwalten.</p>
|
||||
</section>
|
||||
|
||||
<div class="two-column">
|
||||
<form class="card form-card" method="post" action="<?= e(tenant_url($tenant['slug'], 'login')) ?>">
|
||||
<?= csrf_field($csrf) ?>
|
||||
<label>
|
||||
<span>Login-E-Mail</span>
|
||||
<span>E-Mail-Adresse</span>
|
||||
<input type="email" name="email" value="<?= e((string) old('email')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Passwort</span>
|
||||
<input type="password" name="password" required>
|
||||
</label>
|
||||
<button class="button button-primary" type="submit">Standort anmelden</button>
|
||||
<p class="muted">Passwort vergessen? <a class="text-link" href="<?= e($resetRequestUrl) ?>">Reset anfragen</a></p>
|
||||
<button class="button button-primary" type="submit">Anmelden</button>
|
||||
<p class="muted">Passwort vergessen? <a class="text-link" href="<?= e($resetRequestUrl) ?>">Passwort zurücksetzen</a></p>
|
||||
</form>
|
||||
|
||||
<aside class="card" id="reset-help">
|
||||
<span class="eyebrow">Reset & Hilfe</span>
|
||||
<h2>Member-Self-Service bleibt schlank</h2>
|
||||
<p class="muted">Wenn ein Passwort vergessen wurde, prueft das Standortteam die Anfrage im Pilot zunaechst manuell. Die eigene Reset-Seite fuer dieses Login ist bereits vorgesehen.</p>
|
||||
<span class="eyebrow">Zugang</span>
|
||||
<h2>Je nach Rolle buchen oder verwalten</h2>
|
||||
<p class="muted">Mitglieder erfassen ihren Verbrauch. Verantwortliche Personen sehen zusätzlich Mitglieder, Einzahlungen, Papierlisten und Korrekturen.</p>
|
||||
<ul class="feature-list">
|
||||
<li>Mitglieder buchen ueber ihren Standortzugang, nicht ueber die Plattform</li>
|
||||
<li>PIN und RFID sind getrennt vom Passwort-Reset</li>
|
||||
<li>Korrekturen an Salden, Produkten und Papierlisten bleiben beim Standortteam</li>
|
||||
<li>Der Zugang zur Kaffeeliste ist von der Plattformverwaltung getrennt</li>
|
||||
<li>PINs und Karten bleiben vom Passwortwechsel unberührt</li>
|
||||
<li>Bei Fragen hilft die verantwortliche Person der Kaffeeliste</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -1,42 +1,32 @@
|
||||
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
|
||||
|
||||
<section class="page-head">
|
||||
<span class="eyebrow">Standortteam</span>
|
||||
<h1>Mitglieder und Zugaenge</h1>
|
||||
<p>Mitglieder koennen rein fachlich gefuehrt werden oder optional einen Login fuer die Selbstbuchung erhalten. PINs werden nach dem Speichern nicht mehr im Klartext angezeigt.</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<span class="eyebrow">Export & Korrekturen</span>
|
||||
<p class="muted">Hier ist bewusst Platz fuer spaetere CSV-Exporte, Sammelimporte und Statuskorrekturen reserviert, ohne dafuer heute schon neue Controllerlogik zu verlangen.</p>
|
||||
<ul class="feature-list">
|
||||
<li>Mitgliederliste als Export: Platzhalter fuer kuenftigen Download-Link</li>
|
||||
<li>Login-, Rollen- oder PIN-Korrekturen: Platzhalter fuer Hinweise direkt am Datensatz</li>
|
||||
<li>Self-Service-Logins bleiben getrennt von den Backoffice-Feldern des Standortteams</li>
|
||||
</ul>
|
||||
<span class="eyebrow">Kaffeeliste</span>
|
||||
<h1>Mitglieder der Kaffeeliste</h1>
|
||||
<p>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.</p>
|
||||
</section>
|
||||
|
||||
<div class="two-column">
|
||||
<form class="card form-card" method="post" action="<?= e(tenant_url($tenant['slug'], 'members')) ?>">
|
||||
<?= csrf_field($csrf) ?>
|
||||
<label>
|
||||
<span>Anzeigename</span>
|
||||
<span>Name auf der Kaffeeliste</span>
|
||||
<input type="text" name="display_name" value="<?= e((string) old('display_name')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Login-E-Mail fuer Self-Service</span>
|
||||
<span>E-Mail für eigenen Login</span>
|
||||
<input type="email" name="email" value="<?= e((string) old('email')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Startpasswort fuer den ersten Login</span>
|
||||
<span>Startpasswort für Selbstbuchung</span>
|
||||
<input type="password" name="login_password" minlength="12">
|
||||
</label>
|
||||
<label>
|
||||
<span>Optionale PIN fuer Kiosk / Tablet</span>
|
||||
<span>PIN für Kiosk oder Tablet</span>
|
||||
<input type="text" name="access_pin" value="<?= e((string) old('access_pin')) ?>">
|
||||
</label>
|
||||
<button class="button button-primary" type="submit">Mitglied speichern</button>
|
||||
<p class="muted">Wenn eine Login-E-Mail gesetzt ist, wird automatisch ein Member-Login fuer die digitale Selbstbuchung angelegt.</p>
|
||||
<button class="button button-primary" type="submit">Mitglied anlegen</button>
|
||||
<p class="muted">Ohne E-Mail bleibt das Mitglied ein Listenprofil für Nachträge und Papierlisten.</p>
|
||||
</form>
|
||||
|
||||
<section class="table-card">
|
||||
@@ -44,12 +34,17 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Login</th>
|
||||
<th>PIN-Status</th>
|
||||
<th>Selbstbuchung</th>
|
||||
<th>Kiosk-PIN</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($members === []): ?>
|
||||
<tr>
|
||||
<td class="table-empty" colspan="4">Noch keine Mitglieder. Legen Sie das erste Mitglied links im Formular an.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($members as $member): ?>
|
||||
<?php $loginEmail = (string) ($member['login_email'] ?: $member['email']); ?>
|
||||
<?php $hasPin = trim((string) ($member['access_pin'] ?? '')) !== ''; ?>
|
||||
@@ -60,7 +55,7 @@
|
||||
<strong>aktiv</strong><br>
|
||||
<small><?= e($loginEmail) ?></small>
|
||||
<?php else: ?>
|
||||
<span class="muted">nur Listenprofil</span>
|
||||
<span class="muted">nur Verwaltung oder Papierliste</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -1,30 +1,22 @@
|
||||
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
|
||||
|
||||
<section class="page-head">
|
||||
<span class="eyebrow">Backoffice</span>
|
||||
<h1>Papierlisten und Nacherfassung</h1>
|
||||
<p>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.</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<span class="eyebrow">Export & Korrekturen</span>
|
||||
<ul class="feature-list">
|
||||
<li>Druck- oder CSV-Export fuer Papierlisten: Platzhalter fuer spaetere Links</li>
|
||||
<li>Rueckfragen zu einzelnen Positionen: Platzhalter fuer kuenftige Korrekturhinweise am Entwurf</li>
|
||||
<li>Member-Self-Service bleibt von Entwurf, Posting und Nachbearbeitung getrennt</li>
|
||||
</ul>
|
||||
<span class="eyebrow">Papierlisten</span>
|
||||
<h1>Papierliste nachtragen</h1>
|
||||
<p>Übertragen Sie Strichlisten oder Thekenzettel in die Kaffeekasse. Erst beim Abschließen werden die Positionen als Buchungen gezählt.</p>
|
||||
</section>
|
||||
|
||||
<div class="grid-three">
|
||||
<form class="card form-card" method="post" action="<?= e(tenant_url($tenant['slug'], 'paper-sheets')) ?>">
|
||||
<?= csrf_field($csrf) ?>
|
||||
<input type="hidden" name="action" value="create_sheet">
|
||||
<h2>Neue Papierliste</h2>
|
||||
<label>
|
||||
<span>Titel</span>
|
||||
<input type="text" name="title" value="<?= e((string) old('title', 'Wochenliste Kaffee')) ?>" required>
|
||||
<span>Name der Liste</span>
|
||||
<input type="text" name="title" value="<?= e((string) old('title', 'Wochenliste')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Periodenlabel</span>
|
||||
<span>Zeitraum anzeigen als</span>
|
||||
<input type="text" name="period_label" value="<?= e((string) old('period_label', 'KW ' . gmdate('W'))) ?>">
|
||||
</label>
|
||||
<label>
|
||||
@@ -45,6 +37,7 @@
|
||||
<form class="card form-card" method="post" action="<?= e(tenant_url($tenant['slug'], 'paper-sheets')) ?>">
|
||||
<?= csrf_field($csrf) ?>
|
||||
<input type="hidden" name="action" value="add_line">
|
||||
<h2>Position von der Liste übernehmen</h2>
|
||||
<label>
|
||||
<span>Papierliste</span>
|
||||
<select name="paper_sheet_id" required>
|
||||
@@ -76,26 +69,26 @@
|
||||
<input type="number" name="quantity" min="0.25" step="0.25" value="<?= e((string) old('quantity', '1')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Verbrauchszeitpunkt</span>
|
||||
<span>Wann genommen?</span>
|
||||
<input type="datetime-local" name="effective_at" value="<?= e((string) old('effective_at', gmdate('Y-m-d\TH:i'))) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Notiz</span>
|
||||
<input type="text" name="note" value="<?= e((string) old('note')) ?>">
|
||||
</label>
|
||||
<button class="button button-primary" type="submit">Position erfassen</button>
|
||||
<button class="button button-primary" type="submit">Position zur Papierliste hinzufügen</button>
|
||||
</form>
|
||||
|
||||
<section class="card">
|
||||
<span class="eyebrow">Posting</span>
|
||||
<p class="muted">Wenn ein Entwurf vollstaendig ist, wird er mit einem Klick in dieselbe Ledger-Logik uebernommen wie digitale Buchungen.</p>
|
||||
<span class="eyebrow">In Buchungen übernehmen</span>
|
||||
<p class="muted">Prüfen Sie die Positionen. Nach dem Abschließen erscheinen sie als normale Buchungen in der Kaffeekasse.</p>
|
||||
<?php foreach ($sheets as $sheet): ?>
|
||||
<?php if ($sheet['status'] === 'draft'): ?>
|
||||
<form class="inline-form" method="post" action="<?= e(tenant_url($tenant['slug'], 'paper-sheets')) ?>">
|
||||
<?= csrf_field($csrf) ?>
|
||||
<input type="hidden" name="action" value="post_sheet">
|
||||
<input type="hidden" name="paper_sheet_id" value="<?= e((string) $sheet['id']) ?>">
|
||||
<button class="button button-secondary" type="submit"><?= e($sheet['title']) ?> verbuchen</button>
|
||||
<button class="button button-secondary" type="submit"><?= e($sheet['title']) ?> als Buchungen übernehmen</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
@@ -109,14 +102,14 @@
|
||||
<tr>
|
||||
<th>Titel</th>
|
||||
<th>Status</th>
|
||||
<th>Zeilen</th>
|
||||
<th>Positionen</th>
|
||||
<th>Zeitraum</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($sheets === []): ?>
|
||||
<tr>
|
||||
<td colspan="4">Noch keine Papierlisten angelegt.</td>
|
||||
<td class="table-empty" colspan="4">Noch keine Papierlisten. Legen Sie zuerst die erste Liste an.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($sheets as $sheet): ?>
|
||||
@@ -134,7 +127,7 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Papierliste</th>
|
||||
<th>Liste</th>
|
||||
<th>Mitglied</th>
|
||||
<th>Produkt</th>
|
||||
<th>Menge</th>
|
||||
@@ -143,7 +136,7 @@
|
||||
<tbody>
|
||||
<?php if ($sheetLines === []): ?>
|
||||
<tr>
|
||||
<td colspan="4">Noch keine Positionen erfasst.</td>
|
||||
<td class="table-empty" colspan="4">Noch keine Striche oder Positionen übertragen.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($sheetLines as $line): ?>
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
|
||||
|
||||
<section class="page-head">
|
||||
<span class="eyebrow">Backoffice</span>
|
||||
<h1>Einzahlungen verbuchen</h1>
|
||||
<p>Diese Seite bleibt bewusst beim Standortteam. Einzahlungen laufen ins selbe Ledger und reduzieren offene Salden nachvollziehbar, waehrend Mitglieder nur die Selbstbuchung sehen.</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<span class="eyebrow">Export & Korrekturen</span>
|
||||
<ul class="feature-list">
|
||||
<li>Kassenexport oder Monatsabschluss: Platzhalter fuer kuenftige Download-Links</li>
|
||||
<li>Rueckfragen zu Bareinzahlungen: Platzhalter fuer spaetere Korrekturhinweise direkt am Vorgang</li>
|
||||
<li>Member-Self-Service blendet diesen Backoffice-Bereich bewusst aus</li>
|
||||
</ul>
|
||||
<span class="eyebrow">Kaffeekasse</span>
|
||||
<h1>Geld einzahlen</h1>
|
||||
<p>Tragen Sie Bargeld, Überweisungen oder Sammelzahlungen ein. Der offene Betrag des Mitglieds sinkt um diese Einzahlung.</p>
|
||||
</section>
|
||||
|
||||
<div class="two-column">
|
||||
@@ -27,18 +18,18 @@
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Betrag in EUR</span>
|
||||
<span>Eingezahlter Betrag</span>
|
||||
<input type="number" name="amount_eur" min="0.50" step="0.50" value="<?= e((string) old('amount_eur', '5.00')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Zeitpunkt</span>
|
||||
<span>Wann bezahlt?</span>
|
||||
<input type="datetime-local" name="occurred_at" value="<?= e((string) old('occurred_at', gmdate('Y-m-d\TH:i'))) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Notiz</span>
|
||||
<input type="text" name="notes" value="<?= e((string) old('notes')) ?>" placeholder="Barzahlung, Monatsbeitrag, ...">
|
||||
<input type="text" name="notes" value="<?= e((string) old('notes')) ?>" placeholder="Bar, Überweisung, PayPal, Sammelkasse">
|
||||
</label>
|
||||
<button class="button button-primary" type="submit">Einzahlung speichern</button>
|
||||
<button class="button button-primary" type="submit">Einzahlung dem Mitglied gutschreiben</button>
|
||||
</form>
|
||||
|
||||
<section class="table-card">
|
||||
@@ -47,14 +38,14 @@
|
||||
<tr>
|
||||
<th>Zeit</th>
|
||||
<th>Mitglied</th>
|
||||
<th>Beschreibung</th>
|
||||
<th>Zahlungsnotiz</th>
|
||||
<th>Betrag</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($payments === []): ?>
|
||||
<tr>
|
||||
<td colspan="4">Noch keine Einzahlungen verbucht.</td>
|
||||
<td class="table-empty" colspan="4">Noch keine Einzahlungen erfasst. Tragen Sie links die erste Zahlung in die Kaffeekasse ein.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($payments as $payment): ?>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
|
||||
|
||||
<section class="page-head">
|
||||
<span class="eyebrow">Sortiment</span>
|
||||
<span class="eyebrow">Kaffee und Produkte</span>
|
||||
<h1>Produkte und Preise</h1>
|
||||
<p>Die Preislogik bleibt versionierbar. Jede Buchung friert den zum Zeitpunkt gueltigen Preis ein.</p>
|
||||
<p>Legen Sie fest, was gebucht werden kann: Kaffee, Getränke, Snacks oder Pauschalen. Preisänderungen gelten nur für neue Buchungen.</p>
|
||||
</section>
|
||||
|
||||
<div class="two-column">
|
||||
@@ -25,12 +25,17 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Produkt</th>
|
||||
<th>SKU</th>
|
||||
<th>Kurzkennung</th>
|
||||
<th>Preis</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($products === []): ?>
|
||||
<tr>
|
||||
<td class="table-empty" colspan="4">Noch keine Produkte. Legen Sie zuerst z. B. Kaffee, Espresso oder Tee an.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($products as $product): ?>
|
||||
<tr>
|
||||
<td><?= e($product['name']) ?></td>
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
|
||||
|
||||
<section class="page-head">
|
||||
<span class="eyebrow">Pilot & Integration</span>
|
||||
<h1>RFID-Roadmap und Event-Inbox</h1>
|
||||
<p>Das MVP speichert Geraete, Karten und rohe Events bereits produktionsnah, ohne die spaetere Hardwareentscheidung vorwegzunehmen. Dieser Bereich bleibt klar beim Standortteam und der Pilotbetreuung.</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<span class="eyebrow">Export & Korrekturen</span>
|
||||
<ul class="feature-list">
|
||||
<li>Event-Export oder Inbox-Download: Platzhalter fuer spaetere Support-Links</li>
|
||||
<li>Karten- und Geraetekorrekturen: Platzhalter fuer Hinweise direkt an den Tabellen</li>
|
||||
<li>Member-Self-Service blendet RFID-Setup bewusst aus, bis der Pilot weiter ist</li>
|
||||
</ul>
|
||||
<span class="eyebrow">RFID-Kaffeekasse</span>
|
||||
<h1>Kartenleser einrichten</h1>
|
||||
<p>Richten Sie Kartenleser ein und verbinden Sie Karten mit Mitgliedern. Eingelesene Karten erscheinen unten als Ereignisse.</p>
|
||||
</section>
|
||||
|
||||
<div class="grid-three">
|
||||
@@ -20,14 +11,14 @@
|
||||
<?= csrf_field($csrf) ?>
|
||||
<input type="hidden" name="action" value="create_device">
|
||||
<label>
|
||||
<span>Geraetename</span>
|
||||
<span>Name für diesen Leser</span>
|
||||
<input type="text" name="name" value="<?= e((string) old('name')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Standort</span>
|
||||
<span>Ort am Standort</span>
|
||||
<input type="text" name="location_label" value="<?= e((string) old('location_label')) ?>">
|
||||
</label>
|
||||
<button class="button button-primary" type="submit">RFID-Geraet vorbereiten</button>
|
||||
<button class="button button-primary" type="submit">Kartenleser anlegen</button>
|
||||
</form>
|
||||
|
||||
<form class="card form-card" method="post" action="<?= e(tenant_url($tenant['slug'], 'rfid')) ?>">
|
||||
@@ -42,26 +33,26 @@
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Karten-UID</span>
|
||||
<span>Kartennummer</span>
|
||||
<input type="text" name="uid" value="<?= e((string) old('uid')) ?>" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Label</span>
|
||||
<span>Kartenlabel</span>
|
||||
<input type="text" name="label" value="<?= e((string) old('label')) ?>">
|
||||
</label>
|
||||
<button class="button button-primary" type="submit">Karte zuweisen</button>
|
||||
<button class="button button-primary" type="submit">Karte mit Mitglied verbinden</button>
|
||||
</form>
|
||||
|
||||
<section class="card">
|
||||
<span class="eyebrow">API</span>
|
||||
<p class="muted">POST `/api/rfid/intake` mit JSON-Body:</p>
|
||||
<span class="eyebrow">Schnittstelle für den Leser</span>
|
||||
<p class="muted">Der Leser sendet Ereignisse in diesem Format:</p>
|
||||
<pre class="code-block">{
|
||||
"device_token": "...",
|
||||
"uid": "04AABBCCDD",
|
||||
"event_id": "reader-0001",
|
||||
"ts": "2026-06-15T09:30:00Z"
|
||||
}</pre>
|
||||
<p class="muted">Events werden tenant-scharf gespeichert und spaeter in Buchungsregeln ueberfuehrt.</p>
|
||||
<p class="muted">Ein Ereignis ist noch keine Kaffeebuchung. Es zeigt zuerst, welche Karte an welchem Leser erkannt wurde.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -70,16 +61,16 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Geraet</th>
|
||||
<th>Kartenleser</th>
|
||||
<th>Standort</th>
|
||||
<th>Status</th>
|
||||
<th>Token</th>
|
||||
<th>Geräte-Token</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($devices === []): ?>
|
||||
<tr>
|
||||
<td colspan="4">Noch keine RFID-Geraete vorbereitet.</td>
|
||||
<td class="table-empty" colspan="4">Noch kein Kartenleser angelegt. Legen Sie oben den ersten Leser an.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($devices as $device): ?>
|
||||
@@ -106,7 +97,7 @@
|
||||
<tbody>
|
||||
<?php if ($tags === []): ?>
|
||||
<tr>
|
||||
<td colspan="4">Noch keine RFID-Karten zugewiesen.</td>
|
||||
<td class="table-empty" colspan="4">Noch keine Karten mit Mitgliedern verbunden.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($tags as $tag): ?>
|
||||
@@ -127,15 +118,15 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zeit</th>
|
||||
<th>Geraet</th>
|
||||
<th>Kartenleser</th>
|
||||
<th>Status</th>
|
||||
<th>Event-ID</th>
|
||||
<th>Ereignis-ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($events === []): ?>
|
||||
<tr>
|
||||
<td colspan="4">Noch keine RFID-Events empfangen.</td>
|
||||
<td class="table-empty" colspan="4">Noch keine Karte wurde von einem Leser gemeldet.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($events as $event): ?>
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user