Initial Kaffeekasse SaaS restart

This commit is contained in:
2026-06-15 17:13:38 +02:00
commit b08eb93547
54 changed files with 4617 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
<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>
</div>
<form method="post" action="/admin/logout">
<?= csrf_field($csrf) ?>
<button class="button button-secondary" type="submit">Abmelden</button>
</form>
</section>
<section class="table-card">
<table>
<thead>
<tr>
<th>Mandant</th>
<th>Paket</th>
<th>Status</th>
<th>Mitglieder</th>
<th>Produkte</th>
<th>Aktion</th>
</tr>
</thead>
<tbody>
<?php foreach ($tenants as $tenant): ?>
<tr>
<td>
<strong><?= e($tenant['name']) ?></strong><br>
<small>/t/<?= e($tenant['slug']) ?></small>
</td>
<td><?= e(ucfirst($tenant['plan'])) ?></td>
<td><?= e($tenant['status']) ?></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>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</section>
<?php require __DIR__ . '/../partials/layout-bottom.php'; ?>
+22
View File
@@ -0,0 +1,22 @@
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
<section class="page-head">
<span class="eyebrow">Plattform</span>
<h1>Admin-Login</h1>
<p>Fuer Betrieb, Mandantenuebersicht, Support und spaetere Vermarktungssteuerung.</p>
</section>
<form class="card form-card slim" method="post" action="/admin/login">
<?= csrf_field($csrf) ?>
<label>
<span>E-Mail</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">Anmelden</button>
</form>
<?php require __DIR__ . '/../partials/layout-bottom.php'; ?>
+108
View File
@@ -0,0 +1,108 @@
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
<section class="hero">
<div class="hero-copy">
<span class="eyebrow">Von der Papierliste zur produktiven SaaS</span>
<h1>Kaffeeliste, digitale Strichliste und RFID-Roadmap in einem System.</h1>
<p class="lead">Die Anwendung verbindet klassische Kaffeekasse, digitale Selbstbuchung und spaetere Geraeteintegration. Mandanten registrieren sich selbst, verwalten Mitglieder, Produkte, Einzahlungen und Papierlisten in einem PHP/MySQL-Stack, der auf Netcup-Webspace laeuft.</p>
<div class="button-row">
<?php if ($setupComplete): ?>
<a class="button button-primary" href="/register">Mandant starten</a>
<a class="button button-secondary" href="/admin/login">Plattform betreten</a>
<?php else: ?>
<a class="button button-primary" href="/install">Installation starten</a>
<?php endif; ?>
</div>
<ul class="metric-row">
<li><strong>1</strong><span>gemeinsames Ledger fuer digital, Papier und RFID</span></li>
<li><strong>3</strong><span>Pakete fuer Vermarktung und Rollout</span></li>
<li><strong>100%</strong><span>Webspace-kompatibler PHP/MySQL-Betrieb</span></li>
</ul>
</div>
<aside class="hero-card">
<h2>Produktive Ausrichtung</h2>
<ul class="feature-list">
<li>Mandanten-Self-Service mit Owner-Onboarding</li>
<li>Digitale Buchungen fuer Tablet, Browser und Kiosk</li>
<li>Papierlisten als Entwurf, Nacherfassung und Ledger-Posting</li>
<li>Einzahlungen, Salden und Audit-Trail pro Mandant</li>
<li>RFID-Devices, Karten und Event-Inbox fuer die spaetere Automatisierung</li>
</ul>
<?php if (is_array($stats)): ?>
<div class="stats-panel">
<div><strong><?= e((string) $stats['tenants']) ?></strong><span>Mandanten 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>
<?php endif; ?>
</aside>
</section>
<section class="content-grid">
<article class="card">
<span class="eyebrow">Produkt</span>
<h2>Eine Buchungslogik, mehrere Erfassungswege</h2>
<p>Jede Kaffee- oder Tee-Buchung landet im selben Ledger. Ob jemand digital klickt, ob ein Admin eine Papierliste erfasst oder ob spaeter ein RFID-Leser Events einspeist: Salden, Historie und Auswertungen bleiben konsistent.</p>
</article>
<article class="card">
<span class="eyebrow">Marketing</span>
<h2>Klare Positionierung fuer Teams, Vereine und Coworking</h2>
<p>Die Startseite spricht gezielt Organisationen an, die zwischen analoger Kaffeekasse und digitaler Selbstbedienung stehen. Das senkt Erklaerungsaufwand und macht den Upgrade-Pfad zu RFID nachvollziehbar.</p>
</article>
<article class="card">
<span class="eyebrow">Betrieb</span>
<h2>Netcup-tauglich statt hyperskalierter Overkill</h2>
<p>Pfadbasierte Mandanten, gemeinsamer MySQL-Kern, Cron statt Worker und ein sauberer Webroot auf `public/` sind genau auf klassisches Webhosting zugeschnitten.</p>
</article>
</section>
<section class="plans">
<div class="section-head">
<span class="eyebrow">Pakete</span>
<h2>Preismodelle fuer die Vermarktung</h2>
</div>
<div class="plan-grid">
<article class="plan-card">
<h3>Starter</h3>
<p>Fuer kleine Teams und Vereine mit digitaler Buchung und einfacher Kaffeekasse.</p>
<ul>
<li>Mitglieder, Produkte, digitale Buchungen</li>
<li>Einzahlungen und Salden</li>
<li>Self-Service-Registrierung</li>
</ul>
</article>
<article class="plan-card highlighted">
<h3>Team</h3>
<p>Fuer Organisationen, die Papierlisten und digitale Prozesse parallel fahren.</p>
<ul>
<li>Papierlisten-Workflow mit Nacherfassung</li>
<li>Plattform-Dashboard und Audit-Trail</li>
<li>Vorbereitung fuer Standortgeraete</li>
</ul>
</article>
<article class="plan-card">
<h3>Business</h3>
<p>Fuer Standorte mit RFID-Roadmap, Betriebsanforderungen und spaeterem White-Labeling.</p>
<ul>
<li>RFID-Devices, Karten und Intake-API</li>
<li>Release-, Backup- und Deployment-Rahmen</li>
<li>Vorlage fuer spaetere Integrationen</li>
</ul>
</article>
</div>
</section>
<section class="timeline card">
<div class="section-head">
<span class="eyebrow">Roadmap</span>
<h2>Sauberer Pfad in die Produktion</h2>
</div>
<ol class="step-list">
<li>Plattform installieren und ersten Admin anlegen.</li>
<li>Mandant registrieren, Owner-Onboarding abschliessen und Produkte vorbelegen.</li>
<li>Digitale Buchungen und Papierlisten parallel im Team einsetzen.</li>
<li>Mit RFID-Geraeten spaeter ueber die Event-Inbox anbinden, ohne das Kernledger umzubauen.</li>
</ol>
</section>
<?php require __DIR__ . '/../partials/layout-bottom.php'; ?>
+59
View File
@@ -0,0 +1,59 @@
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
<section class="page-head">
<span class="eyebrow">Self-Service</span>
<h1>Mandant registrieren</h1>
<p>Der Owner legt den ersten Standort an und bekommt automatisch die Grundbausteine fuer digitale Buchung, Papierliste und spaetere RFID-Anbindung.</p>
</section>
<div class="two-column">
<form class="card form-card" method="post" action="/register">
<?= csrf_field($csrf) ?>
<label>
<span>Organisationsname</span>
<input type="text" name="tenant_name" value="<?= e((string) old('tenant_name')) ?>" required>
</label>
<label>
<span>Kurzname / URL-Slug</span>
<input type="text" name="tenant_slug" value="<?= e((string) old('tenant_slug')) ?>" placeholder="mein-team">
</label>
<label>
<span>Paket</span>
<select name="plan">
<option value="starter">Starter</option>
<option value="team" selected>Team</option>
<option value="business">Business</option>
</select>
</label>
<label>
<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>Owner-Name</span>
<input type="text" name="owner_name" value="<?= e((string) old('owner_name')) ?>" required>
</label>
<label>
<span>Owner-E-Mail</span>
<input type="email" name="owner_email" value="<?= e((string) old('owner_email')) ?>" required>
</label>
<label>
<span>Owner-Passwort</span>
<input type="password" name="owner_password" minlength="12" required>
</label>
<button class="button button-primary" type="submit">Mandant anlegen</button>
</form>
<aside class="card">
<span class="eyebrow">Was automatisch entsteht</span>
<ul class="feature-list">
<li>Tenant mit Owner-Rolle</li>
<li>Systemquellen fuer digital, Papier, Backoffice und RFID</li>
<li>Standardprodukte Kaffee, Espresso und Tee</li>
<li>Voreinstellung fuer Self-Service und Papierlisten</li>
</ul>
<p class="muted">Damit ist die Vermarktung sauber: Interessenten landen nicht in einem Technikgeruest, sondern in einem sofort nutzbaren Grundaufbau.</p>
</aside>
</div>
<?php require __DIR__ . '/../partials/layout-bottom.php'; ?>
+56
View File
@@ -0,0 +1,56 @@
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
<section class="page-head">
<span class="eyebrow">Setup</span>
<h1>Plattform installieren</h1>
<p>Der Installer schreibt `.env`, legt das MySQL-Schema an und erstellt den ersten Plattform-Admin. Danach ist das Self-Service-Onboarding fuer Mandanten freigeschaltet.</p>
</section>
<form class="card form-card" method="post" action="/install">
<?= csrf_field($csrf) ?>
<div class="form-grid">
<label>
<span>App-URL</span>
<input type="url" name="app_url" value="<?= e((string) old('app_url', $config['url'])) ?>" required>
</label>
<label>
<span>DB-Host</span>
<input type="text" name="db_host" value="<?= e((string) old('db_host', '127.0.0.1')) ?>" required>
</label>
<label>
<span>DB-Port</span>
<input type="number" name="db_port" value="<?= e((string) old('db_port', '3306')) ?>" required>
</label>
<label>
<span>DB-Name</span>
<input type="text" name="db_name" value="<?= e((string) old('db_name')) ?>" required>
</label>
<label>
<span>DB-User</span>
<input type="text" name="db_user" value="<?= e((string) old('db_user')) ?>" required>
</label>
<label>
<span>DB-Passwort</span>
<input type="password" name="db_pass" value="<?= e((string) old('db_pass')) ?>">
</label>
<label>
<span>Mail-Absender</span>
<input type="email" name="mail_from" value="<?= e((string) old('mail_from', 'noreply@example.com')) ?>">
</label>
<label>
<span>Admin-Name</span>
<input type="text" name="admin_name" value="<?= e((string) old('admin_name')) ?>" required>
</label>
<label>
<span>Admin-E-Mail</span>
<input type="email" name="admin_email" value="<?= e((string) old('admin_email')) ?>" required>
</label>
<label>
<span>Admin-Passwort</span>
<input type="password" name="admin_password" minlength="12" required>
</label>
</div>
<button class="button button-primary" type="submit">Installation abschliessen</button>
</form>
<?php require __DIR__ . '/../partials/layout-bottom.php'; ?>
@@ -0,0 +1,16 @@
</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>
</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>
</div>
</div>
</footer>
</body>
</html>
<?php clear_old_input(); ?>
+50
View File
@@ -0,0 +1,50 @@
<?php
$flashSuccess = flash($session, 'success');
$flashError = flash($session, 'error');
$authState = $authState ?? $session->get('auth');
$tenant = $tenant ?? null;
$currentPath = current_path();
?>
<!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>
<link rel="stylesheet" href="<?= e(asset_url('app.css')) ?>">
</head>
<body class="<?= e($pageClass ?? 'app-shell') ?>">
<div class="page-backdrop"></div>
<header class="site-header">
<div class="shell header-inner">
<a class="brand" href="/">
<span class="brand-mark">KK</span>
<span>
<strong><?= e($config['name']) ?></strong>
<small>Mandantenfaehige Kaffeekasse</small>
</span>
</a>
<nav class="top-nav">
<?php if (is_array($tenant) && isset($tenant['slug'])): ?>
<a href="<?= e(tenant_url($tenant['slug'])) ?>" class="<?= $currentPath === tenant_url($tenant['slug']) ? 'active' : '' ?>">Dashboard</a>
<a href="<?= e(tenant_url($tenant['slug'], 'members')) ?>" class="<?= str_contains($currentPath, '/members') ? 'active' : '' ?>">Mitglieder</a>
<a href="<?= e(tenant_url($tenant['slug'], 'products')) ?>" class="<?= str_contains($currentPath, '/products') ? 'active' : '' ?>">Produkte</a>
<a href="<?= e(tenant_url($tenant['slug'], 'bookings')) ?>" class="<?= str_contains($currentPath, '/bookings') ? 'active' : '' ?>">Digital</a>
<a href="<?= e(tenant_url($tenant['slug'], 'payments')) ?>" class="<?= str_contains($currentPath, '/payments') ? 'active' : '' ?>">Einzahlungen</a>
<a href="<?= e(tenant_url($tenant['slug'], 'paper-sheets')) ?>" class="<?= str_contains($currentPath, '/paper-sheets') ? 'active' : '' ?>">Papier</a>
<a href="<?= e(tenant_url($tenant['slug'], 'rfid')) ?>" class="<?= str_contains($currentPath, '/rfid') ? 'active' : '' ?>">RFID</a>
<?php else: ?>
<a href="/">Start</a>
<a href="/register">Registrieren</a>
<a href="/admin/login">Plattform</a>
<?php endif; ?>
</nav>
</div>
</header>
<main class="shell main-shell">
<?php if ($flashSuccess): ?>
<div class="flash flash-success"><?= e((string) $flashSuccess) ?></div>
<?php endif; ?>
<?php if ($flashError): ?>
<div class="flash flash-error"><?= e((string) $flashError) ?></div>
<?php endif; ?>
+76
View File
@@ -0,0 +1,76 @@
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
<section class="page-head">
<span class="eyebrow">Selbstbedienung</span>
<h1>Digitale Buchungen</h1>
<p>Dies ist der produktive Kern fuer Browser, Tablet und spaetere Kiosk-Workflows.</p>
</section>
<div class="two-column">
<form class="card form-card" method="post" action="<?= e(tenant_url($tenant['slug'], 'bookings')) ?>">
<?= csrf_field($csrf) ?>
<label>
<span>Mitglied</span>
<select name="member_id" required>
<?php foreach ($members as $member): ?>
<option value="<?= e((string) $member['id']) ?>"><?= e($member['display_name']) ?></option>
<?php endforeach; ?>
</select>
</label>
<label>
<span>Produkt</span>
<select name="product_id" required>
<?php foreach ($products as $product): ?>
<option value="<?= e((string) $product['id']) ?>"><?= e($product['name']) ?> (<?= e(money_from_cents((int) $product['price_cents'])) ?>)</option>
<?php endforeach; ?>
</select>
</label>
<label>
<span>Quelle</span>
<select name="source_code">
<option value="digital_self">Digitale Selbstbuchung</option>
<option value="admin_backoffice">Backoffice-Buchung</option>
</select>
</label>
<label>
<span>Menge</span>
<input type="number" name="quantity" min="0.25" step="0.25" value="<?= e((string) old('quantity', '1')) ?>" required>
</label>
<label>
<span>Verbrauchszeitpunkt</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>
<input type="text" name="notes" value="<?= e((string) old('notes')) ?>">
</label>
<button class="button button-primary" type="submit">Buchung speichern</button>
</form>
<section class="table-card">
<table>
<thead>
<tr>
<th>Zeit</th>
<th>Mitglied</th>
<th>Produkt</th>
<th>Quelle</th>
<th>Betrag</th>
</tr>
</thead>
<tbody>
<?php foreach ($bookings as $booking): ?>
<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(money_from_cents((int) $booking['total_cents'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</section>
</div>
<?php require __DIR__ . '/../partials/layout-bottom.php'; ?>
+60
View File
@@ -0,0 +1,60 @@
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
<section class="page-head inline-head">
<div>
<span class="eyebrow"><?= e(ucfirst($tenant['plan'])) ?> Paket</span>
<h1><?= e($tenant['name']) ?></h1>
<p>Rolle: <?= e((string) ($authState['tenant_role'] ?? 'member')) ?>. Hier laufen digitale Buchungen, Papierlisten und Betriebszahlen zusammen.</p>
</div>
<form method="post" action="<?= e(tenant_url($tenant['slug'], 'logout')) ?>">
<?= csrf_field($csrf) ?>
<button class="button button-secondary" type="submit">Abmelden</button>
</form>
</section>
<section class="metric-cards">
<article class="metric-card">
<span>Mitglieder</span>
<strong><?= e((string) $summary['member_count']) ?></strong>
</article>
<article class="metric-card">
<span>Produkte</span>
<strong><?= e((string) $summary['product_count']) ?></strong>
</article>
<article class="metric-card">
<span>Papierlisten</span>
<strong><?= e((string) $summary['paper_sheet_count']) ?></strong>
</article>
<article class="metric-card">
<span>Offene Salden</span>
<strong><?= e(money_from_cents((int) $summary['open_balance_cents'])) ?></strong>
</article>
</section>
<div class="two-column">
<section class="card">
<span class="eyebrow">Letzte Buchungen</span>
<ul class="stack-list">
<?php foreach ($latestEvents as $event): ?>
<li>
<strong><?= e($event['member_name']) ?></strong>
<span><?= e($event['product_name']) ?> via <?= e($event['source_name']) ?></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>
<ul class="stack-list">
<?php foreach ($memberBalances as $balance): ?>
<li>
<strong><?= e($balance['display_name']) ?></strong>
<small><?= e(money_from_cents((int) $balance['balance_cents'])) ?></small>
</li>
<?php endforeach; ?>
</ul>
</section>
</div>
<?php require __DIR__ . '/../partials/layout-bottom.php'; ?>
+22
View File
@@ -0,0 +1,22 @@
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
<section class="page-head">
<span class="eyebrow">Mandant</span>
<h1><?= e($tenant['name']) ?></h1>
<p>Digitale Strichliste, Papierlisten-Nacherfassung und spaetere RFID-Anbindung fuer diesen Standort.</p>
</section>
<form class="card form-card slim" method="post" action="<?= e(tenant_url($tenant['slug'], 'login')) ?>">
<?= csrf_field($csrf) ?>
<label>
<span>E-Mail</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">Anmelden</button>
</form>
<?php require __DIR__ . '/../partials/layout-bottom.php'; ?>
+55
View File
@@ -0,0 +1,55 @@
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
<section class="page-head">
<span class="eyebrow">Stammdaten</span>
<h1>Mitglieder verwalten</h1>
<p>Mitglieder koennen rein fachlich gefuehrt werden oder optional einen Login fuer die digitale Buchung erhalten.</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>
<input type="text" name="display_name" value="<?= e((string) old('display_name')) ?>" required>
</label>
<label>
<span>E-Mail fuer Login</span>
<input type="email" name="email" value="<?= e((string) old('email')) ?>">
</label>
<label>
<span>Login-Passwort</span>
<input type="password" name="login_password" minlength="12">
</label>
<label>
<span>PIN fuer Kiosk / 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>
</form>
<section class="table-card">
<table>
<thead>
<tr>
<th>Name</th>
<th>E-Mail</th>
<th>PIN</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($members as $member): ?>
<tr>
<td><?= e($member['display_name']) ?></td>
<td><?= e((string) ($member['login_email'] ?: $member['email'])) ?></td>
<td><?= e((string) ($member['access_pin'] ?? '')) ?></td>
<td><?= e($member['status']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</section>
</div>
<?php require __DIR__ . '/../partials/layout-bottom.php'; ?>
+143
View File
@@ -0,0 +1,143 @@
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
<section class="page-head">
<span class="eyebrow">Klassische Liste</span>
<h1>Papierlisten und Nacherfassung</h1>
<p>Die klassische Kaffeeliste bleibt erhalten, wird aber als geordneter Beleg mit sauberem Posting ins Ledger behandelt.</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">
<label>
<span>Titel</span>
<input type="text" name="title" value="<?= e((string) old('title', 'Wochenliste Kaffee')) ?>" required>
</label>
<label>
<span>Periodenlabel</span>
<input type="text" name="period_label" value="<?= e((string) old('period_label', 'KW ' . gmdate('W'))) ?>">
</label>
<label>
<span>Startdatum</span>
<input type="date" name="period_start" value="<?= e((string) old('period_start', gmdate('Y-m-01'))) ?>">
</label>
<label>
<span>Enddatum</span>
<input type="date" name="period_end" value="<?= e((string) old('period_end', gmdate('Y-m-t'))) ?>">
</label>
<label>
<span>Notiz</span>
<input type="text" name="notes" value="<?= e((string) old('notes')) ?>">
</label>
<button class="button button-primary" type="submit">Papierliste anlegen</button>
</form>
<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">
<label>
<span>Papierliste</span>
<select name="paper_sheet_id" required>
<?php foreach ($sheets as $sheet): ?>
<?php if ($sheet['status'] === 'draft'): ?>
<option value="<?= e((string) $sheet['id']) ?>"><?= e($sheet['title']) ?> (Entwurf)</option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</label>
<label>
<span>Mitglied</span>
<select name="member_id" required>
<?php foreach ($members as $member): ?>
<option value="<?= e((string) $member['id']) ?>"><?= e($member['display_name']) ?></option>
<?php endforeach; ?>
</select>
</label>
<label>
<span>Produkt</span>
<select name="product_id" required>
<?php foreach ($products as $product): ?>
<option value="<?= e((string) $product['id']) ?>"><?= e($product['name']) ?></option>
<?php endforeach; ?>
</select>
</label>
<label>
<span>Menge</span>
<input type="number" name="quantity" min="0.25" step="0.25" value="<?= e((string) old('quantity', '1')) ?>" required>
</label>
<label>
<span>Verbrauchszeitpunkt</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>
</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>
<?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>
</form>
<?php endif; ?>
<?php endforeach; ?>
</section>
</div>
<div class="two-column">
<section class="table-card">
<table>
<thead>
<tr>
<th>Titel</th>
<th>Status</th>
<th>Zeilen</th>
<th>Zeitraum</th>
</tr>
</thead>
<tbody>
<?php foreach ($sheets as $sheet): ?>
<tr>
<td><?= e($sheet['title']) ?></td>
<td><?= e($sheet['status']) ?></td>
<td><?= e((string) $sheet['line_count']) ?></td>
<td><?= e((string) $sheet['period_label']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</section>
<section class="table-card">
<table>
<thead>
<tr>
<th>Papierliste</th>
<th>Mitglied</th>
<th>Produkt</th>
<th>Menge</th>
</tr>
</thead>
<tbody>
<?php foreach ($sheetLines as $line): ?>
<tr>
<td>#<?= e((string) $line['paper_sheet_id']) ?></td>
<td><?= e($line['member_name']) ?></td>
<td><?= e($line['product_name']) ?></td>
<td><?= e((string) $line['quantity']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</section>
</div>
<?php require __DIR__ . '/../partials/layout-bottom.php'; ?>
+59
View File
@@ -0,0 +1,59 @@
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
<section class="page-head">
<span class="eyebrow">Finanzen</span>
<h1>Einzahlungen verbuchen</h1>
<p>Einzahlungen laufen ins selbe Ledger und reduzieren den offenen Saldo des Mitglieds nachvollziehbar.</p>
</section>
<div class="two-column">
<form class="card form-card" method="post" action="<?= e(tenant_url($tenant['slug'], 'payments')) ?>">
<?= csrf_field($csrf) ?>
<label>
<span>Mitglied</span>
<select name="member_id" required>
<?php foreach ($members as $member): ?>
<option value="<?= e((string) $member['id']) ?>"><?= e($member['display_name']) ?></option>
<?php endforeach; ?>
</select>
</label>
<label>
<span>Betrag in EUR</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>
<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, ...">
</label>
<button class="button button-primary" type="submit">Einzahlung speichern</button>
</form>
<section class="table-card">
<table>
<thead>
<tr>
<th>Zeit</th>
<th>Mitglied</th>
<th>Beschreibung</th>
<th>Betrag</th>
</tr>
</thead>
<tbody>
<?php foreach ($payments as $payment): ?>
<tr>
<td><?= e($payment['occurred_at']) ?></td>
<td><?= e((string) $payment['member_name']) ?></td>
<td><?= e($payment['description']) ?></td>
<td><?= e(money_from_cents((int) $payment['amount_cents'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</section>
</div>
<?php require __DIR__ . '/../partials/layout-bottom.php'; ?>
+47
View File
@@ -0,0 +1,47 @@
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
<section class="page-head">
<span class="eyebrow">Sortiment</span>
<h1>Produkte und Preise</h1>
<p>Die Preislogik bleibt versionierbar. Jede Buchung friert den zum Zeitpunkt gueltigen Preis ein.</p>
</section>
<div class="two-column">
<form class="card form-card" method="post" action="<?= e(tenant_url($tenant['slug'], 'products')) ?>">
<?= csrf_field($csrf) ?>
<label>
<span>Produktname</span>
<input type="text" name="name" value="<?= e((string) old('name')) ?>" required>
</label>
<label>
<span>Preis in EUR</span>
<input type="number" name="price_eur" min="0.10" step="0.05" value="<?= e((string) old('price_eur', '1.20')) ?>" required>
</label>
<button class="button button-primary" type="submit">Produkt anlegen</button>
</form>
<section class="table-card">
<table>
<thead>
<tr>
<th>Produkt</th>
<th>SKU</th>
<th>Preis</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($products as $product): ?>
<tr>
<td><?= e($product['name']) ?></td>
<td><?= e((string) $product['sku']) ?></td>
<td><?= e(money_from_cents((int) ($product['price_cents'] ?? 0))) ?></td>
<td><?= $product['is_active'] ? 'aktiv' : 'inaktiv' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</section>
</div>
<?php require __DIR__ . '/../partials/layout-bottom.php'; ?>
+129
View File
@@ -0,0 +1,129 @@
<?php require __DIR__ . '/../partials/layout-top.php'; ?>
<section class="page-head">
<span class="eyebrow">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.</p>
</section>
<div class="grid-three">
<form class="card form-card" method="post" action="<?= e(tenant_url($tenant['slug'], 'rfid')) ?>">
<?= csrf_field($csrf) ?>
<input type="hidden" name="action" value="create_device">
<label>
<span>Geraetename</span>
<input type="text" name="name" value="<?= e((string) old('name')) ?>" required>
</label>
<label>
<span>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>
</form>
<form class="card form-card" method="post" action="<?= e(tenant_url($tenant['slug'], 'rfid')) ?>">
<?= csrf_field($csrf) ?>
<input type="hidden" name="action" value="assign_tag">
<label>
<span>Mitglied</span>
<select name="member_id" required>
<?php foreach ($members as $member): ?>
<option value="<?= e((string) $member['id']) ?>"><?= e($member['display_name']) ?></option>
<?php endforeach; ?>
</select>
</label>
<label>
<span>Karten-UID</span>
<input type="text" name="uid" value="<?= e((string) old('uid')) ?>" required>
</label>
<label>
<span>Label</span>
<input type="text" name="label" value="<?= e((string) old('label')) ?>">
</label>
<button class="button button-primary" type="submit">Karte zuweisen</button>
</form>
<section class="card">
<span class="eyebrow">API</span>
<p class="muted">POST `/api/rfid/intake` mit JSON-Body:</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>
</section>
</div>
<div class="two-column">
<section class="table-card">
<table>
<thead>
<tr>
<th>Geraet</th>
<th>Standort</th>
<th>Status</th>
<th>Token</th>
</tr>
</thead>
<tbody>
<?php foreach ($devices as $device): ?>
<tr>
<td><?= e($device['name']) ?></td>
<td><?= e((string) $device['location_label']) ?></td>
<td><?= e($device['status']) ?></td>
<td><code><?= e(substr($device['device_token'], 0, 16)) ?>...</code></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</section>
<section class="table-card">
<table>
<thead>
<tr>
<th>Karte</th>
<th>Mitglied</th>
<th>Status</th>
<th>Zuletzt</th>
</tr>
</thead>
<tbody>
<?php foreach ($tags as $tag): ?>
<tr>
<td><?= e((string) ($tag['label'] ?: 'ohne Label')) ?></td>
<td><?= e((string) $tag['member_name']) ?></td>
<td><?= e($tag['status']) ?></td>
<td><?= e((string) ($tag['last_seen_at'] ?? 'noch nie')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</section>
</div>
<section class="table-card">
<table>
<thead>
<tr>
<th>Zeit</th>
<th>Geraet</th>
<th>Status</th>
<th>Event-ID</th>
</tr>
</thead>
<tbody>
<?php foreach ($events as $event): ?>
<tr>
<td><?= e($event['received_at']) ?></td>
<td><?= e((string) $event['device_name']) ?></td>
<td><?= e($event['status']) ?></td>
<td><?= e((string) $event['external_event_id']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</section>
<?php require __DIR__ . '/../partials/layout-bottom.php'; ?>