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
+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'; ?>