Initial Kaffeekasse SaaS restart
This commit is contained in:
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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(); ?>
|
||||
@@ -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; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
Reference in New Issue
Block a user