- Implemented the full Fat Bottom Grille storefront and staff dashboard.
Highlights include configurable menus, specials, cart/checkout, taxes, customer accounts with email 2FA, newsletters, analytics, refunds, PDF/CSV exports, Square/Mailgun adapters, and optional MySQL migration. See README.md for setup and production configuration. Verified PHP/JavaScript syntax, responsive layouts, registration, 2FA, checkout, dashboard workflows, settings persistence, and PDF generation. Real Square, Mailgun, and MySQL connections require credentials/server access. Square implementation follows the official Payments API and Refunds API.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
<div class="stat-grid">
|
||||
<article class="stat-card stat-primary">
|
||||
<span>Today’s sales</span>
|
||||
<strong><?= money($stats['today_sales']) ?></strong>
|
||||
<small><?= (int) $stats['today_orders'] ?> paid orders</small>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<span>Open orders</span>
|
||||
<strong><?= (int) $stats['open_orders'] ?></strong>
|
||||
<small>Paid through ready</small>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<span>Abandoned carts</span>
|
||||
<strong><?= (int) $stats['abandoned_carts'] ?></strong>
|
||||
<small>Inactive for 30+ minutes</small>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<span>Today’s visitors</span>
|
||||
<strong><?= (int) $stats['today_visitors'] ?></strong>
|
||||
<small><?= (int) $stats['newsletter_members'] ?> news subscribers</small>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<?php if (password_verify('ChangeMe123!', (string) $currentUser['password_hash'])): ?>
|
||||
<div class="notice admin-security-notice">
|
||||
<strong>Security reminder:</strong> the initial administrator password is still active.
|
||||
Change it from <a href="/account">My Account</a> before deployment.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="admin-dashboard-grid">
|
||||
<section class="admin-panel admin-panel-wide">
|
||||
<div class="admin-panel-heading">
|
||||
<div>
|
||||
<span class="eyebrow">Live queue</span>
|
||||
<h2>Recent Orders</h2>
|
||||
</div>
|
||||
<a class="button button-small" href="/admin/orders">View All Orders</a>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Order</th><th>Customer</th><th>Placed</th><th>Status</th><th>Total</th></tr></thead>
|
||||
<tbody>
|
||||
<?php if (!$recentOrders): ?>
|
||||
<tr><td colspan="5" class="empty-cell">No orders yet. The kitchen is standing by.</td></tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($recentOrders as $order): ?>
|
||||
<tr>
|
||||
<td><a href="/admin/order?id=<?= (int) $order['id'] ?>"><?= e($order['order_number']) ?></a></td>
|
||||
<td><?= e($order['customer_name']) ?><small><?= e($order['customer_phone']) ?></small></td>
|
||||
<td><?= e(store_datetime((string) $order['placed_at'], 'g:i A')) ?></td>
|
||||
<td><span class="status-pill status-<?= e($order['status']) ?>"><?= e(ucfirst((string) $order['status'])) ?></span></td>
|
||||
<td><?= money($order['total_cents']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading">
|
||||
<div>
|
||||
<span class="eyebrow">Last 7 days</span>
|
||||
<h2>Store Traffic</h2>
|
||||
</div>
|
||||
</div>
|
||||
<?php $maxTraffic = max(1, ...array_map(static fn (array $row): int => (int) $row['visitors'], $traffic)); ?>
|
||||
<div class="bar-chart">
|
||||
<?php if (!$traffic): ?><p class="muted">Traffic data will appear as visitors arrive.</p><?php endif; ?>
|
||||
<?php foreach ($traffic as $day): ?>
|
||||
<div>
|
||||
<span class="bar-value"><?= (int) $day['visitors'] ?></span>
|
||||
<i style="height:<?= max(8, round(((int) $day['visitors'] / $maxTraffic) * 120)) ?>px"></i>
|
||||
<small><?= e((new DateTimeImmutable((string) $day['day']))->format('D')) ?></small>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading">
|
||||
<div>
|
||||
<span class="eyebrow">What’s moving</span>
|
||||
<h2>Popular Items</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-list">
|
||||
<?php if (!$popularItems): ?><p class="muted">Item sales will appear after the first order.</p><?php endif; ?>
|
||||
<?php foreach ($popularItems as $index => $item): ?>
|
||||
<div>
|
||||
<span><?= $index + 1 ?></span>
|
||||
<strong><?= e($item['item_name']) ?></strong>
|
||||
<small><?= (int) $item['quantity'] ?> sold</small>
|
||||
<b><?= money($item['sales_cents']) ?></b>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,125 @@
|
||||
<div class="admin-page-actions">
|
||||
<p>Categories and items appear on the storefront as soon as they are active.</p>
|
||||
<a class="button button-ghost" href="/admin/export/menu.pdf">Download Styled PDF</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-stack">
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading">
|
||||
<div><span class="eyebrow">Organize the storefront</span><h2>Menu Categories</h2></div>
|
||||
<span class="count-badge"><?= count($categories) ?> categories</span>
|
||||
</div>
|
||||
<div class="editable-list">
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<details>
|
||||
<summary>
|
||||
<span class="drag-index"><?= (int) $category['display_order'] ?></span>
|
||||
<span><strong><?= e($category['name']) ?></strong><small><?= e($category['description']) ?></small></span>
|
||||
<span class="status-dot <?= $category['active'] ? 'is-on' : '' ?>"><?= $category['active'] ? 'Active' : 'Hidden' ?></span>
|
||||
<span class="edit-label">Edit</span>
|
||||
</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/category/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int) $category['id'] ?>">
|
||||
<div class="form-grid">
|
||||
<label>Name<input type="text" name="name" required value="<?= e($category['name']) ?>"></label>
|
||||
<label>URL slug<input type="text" name="slug" required value="<?= e($category['slug']) ?>"></label>
|
||||
<label class="field-wide">Description<input type="text" name="description" value="<?= e($category['description']) ?>"></label>
|
||||
<label>Display order<input type="number" name="display_order" value="<?= (int) $category['display_order'] ?>"></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" <?= checked($category['active']) ?>><span><strong>Visible</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Save Category</button>
|
||||
</form>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<details class="new-record">
|
||||
<summary>+ Add Category</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/category/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="form-grid">
|
||||
<label>Name<input type="text" name="name" required></label>
|
||||
<label>URL slug<input type="text" name="slug" placeholder="Generated from name if blank"></label>
|
||||
<label class="field-wide">Description<input type="text" name="description"></label>
|
||||
<label>Display order<input type="number" name="display_order" value="60"></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" checked><span><strong>Visible</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Create Category</button>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading">
|
||||
<div><span class="eyebrow">Price and availability</span><h2>Menu Items</h2></div>
|
||||
<span class="count-badge"><?= count($items) ?> items</span>
|
||||
</div>
|
||||
<div class="editable-list item-editable-list">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<?php $tags = json_decode((string) $item['dietary_tags'], true); ?>
|
||||
<details>
|
||||
<summary>
|
||||
<span class="item-avatar"><?= e(strtoupper(substr((string) $item['name'], 0, 2))) ?></span>
|
||||
<span><strong><?= e($item['name']) ?></strong><small><?= e($item['category_name']) ?></small></span>
|
||||
<strong class="item-price"><?= money($item['price_cents']) ?></strong>
|
||||
<span class="status-dot <?= $item['active'] ? 'is-on' : '' ?>"><?= $item['active'] ? 'Active' : 'Hidden' ?></span>
|
||||
<span class="edit-label">Edit</span>
|
||||
</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/item/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int) $item['id'] ?>">
|
||||
<div class="form-grid form-grid-three">
|
||||
<label>Name<input type="text" name="name" required value="<?= e($item['name']) ?>"></label>
|
||||
<label>Category
|
||||
<select name="category_id" required>
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<option value="<?= (int) $category['id'] ?>" <?= selected($item['category_id'], $category['id']) ?>><?= e($category['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label>Price<input type="number" name="price" min="0" step="0.01" required value="<?= number_format((int) $item['price_cents'] / 100, 2, '.', '') ?>"></label>
|
||||
<label>Compare-at price<input type="number" name="compare_at_price" min="0" step="0.01" value="<?= $item['compare_at_cents'] !== null ? number_format((int) $item['compare_at_cents'] / 100, 2, '.', '') : '' ?>"></label>
|
||||
<label>URL slug<input type="text" name="slug" value="<?= e($item['slug']) ?>"></label>
|
||||
<label>Display order<input type="number" name="display_order" value="<?= (int) $item['display_order'] ?>"></label>
|
||||
<label class="field-wide">Description<textarea name="description" rows="2"><?= e($item['description']) ?></textarea></label>
|
||||
<label class="field-wide">Image URL<input type="url" name="image_url" value="<?= e($item['image_url']) ?>" placeholder="Optional HTTPS image"></label>
|
||||
<label class="field-wide">Tags, comma separated<input type="text" name="dietary_tags" value="<?= e(is_array($tags) ? implode(', ', $tags) : '') ?>"></label>
|
||||
</div>
|
||||
<div class="check-row">
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" <?= checked($item['active']) ?>><span><strong>Available</strong></span></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="featured" value="1" <?= checked($item['featured']) ?>><span><strong>Featured</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Save Item</button>
|
||||
</form>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<details class="new-record">
|
||||
<summary>+ Add Menu Item</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/item/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="form-grid form-grid-three">
|
||||
<label>Name<input type="text" name="name" required></label>
|
||||
<label>Category
|
||||
<select name="category_id" required>
|
||||
<?php foreach ($categories as $category): ?><option value="<?= (int) $category['id'] ?>"><?= e($category['name']) ?></option><?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label>Price<input type="number" name="price" min="0" step="0.01" required></label>
|
||||
<label>Compare-at price<input type="number" name="compare_at_price" min="0" step="0.01"></label>
|
||||
<label>URL slug<input type="text" name="slug" placeholder="Generated from name"></label>
|
||||
<label>Display order<input type="number" name="display_order" value="0"></label>
|
||||
<label class="field-wide">Description<textarea name="description" rows="2"></textarea></label>
|
||||
<label class="field-wide">Image URL<input type="url" name="image_url"></label>
|
||||
<label class="field-wide">Tags, comma separated<input type="text" name="dietary_tags"></label>
|
||||
</div>
|
||||
<div class="check-row">
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" checked><span><strong>Available</strong></span></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="featured" value="1"><span><strong>Featured</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Create Item</button>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<div class="admin-page-actions">
|
||||
<p><strong><?= (int) $recipientCount ?></strong> active users are currently opted in.</p>
|
||||
</div>
|
||||
|
||||
<div class="admin-detail-grid newsletter-grid">
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">Send an update</span><h2>New Newsletter</h2></div></div>
|
||||
<form class="form-stack" action="/admin/newsletters/send" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<label>Subject line<input type="text" name="subject" maxlength="140" required placeholder="This weekend at Fat Bottom Grille"></label>
|
||||
<label>Message<textarea name="content" rows="12" required placeholder="Share a new special, schedule update, or note from the kitchen..."></textarea></label>
|
||||
<div class="notice">Every email includes an unsubscribe link. Users can also opt out from My Account.</div>
|
||||
<button class="button button-large button-block" type="submit" data-confirm="Send this newsletter to every opted-in user?">Send to <?= (int) $recipientCount ?> Subscribers</button>
|
||||
</form>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">Delivery history</span><h2>Past Sends</h2></div></div>
|
||||
<div class="newsletter-history">
|
||||
<?php if (!$newsletters): ?><p class="muted">No newsletters have been sent yet.</p><?php endif; ?>
|
||||
<?php foreach ($newsletters as $newsletter): ?>
|
||||
<article>
|
||||
<div><strong><?= e($newsletter['subject']) ?></strong><span><?= e(store_datetime((string) ($newsletter['sent_at'] ?: $newsletter['created_at']))) ?></span></div>
|
||||
<span class="role-badge"><?= (int) $newsletter['recipient_count'] ?> sent</span>
|
||||
<small>by <?= e($newsletter['author'] ?: 'System') ?></small>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,101 @@
|
||||
<div class="detail-heading">
|
||||
<div>
|
||||
<a class="back-link" href="/admin/orders">← Back to orders</a>
|
||||
<div class="detail-title-row">
|
||||
<h2><?= e($order['order_number']) ?></h2>
|
||||
<span class="status-pill status-<?= e($order['status']) ?>"><?= e(ucfirst((string) $order['status'])) ?></span>
|
||||
</div>
|
||||
<p>Placed <?= e(store_datetime((string) $order['placed_at'], 'F j, Y \a\t g:i A')) ?></p>
|
||||
</div>
|
||||
<strong class="detail-total"><?= money($order['total_cents']) ?></strong>
|
||||
</div>
|
||||
|
||||
<div class="admin-detail-grid">
|
||||
<div>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><h2>Order Items</h2></div>
|
||||
<div class="order-item-list">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<div>
|
||||
<span class="item-quantity"><?= (int) $item['quantity'] ?>x</span>
|
||||
<span><strong><?= e($item['item_name']) ?></strong><?php if ($item['notes']): ?><small><?= e($item['notes']) ?></small><?php endif; ?></span>
|
||||
<strong><?= money($item['line_total_cents']) ?></strong>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<dl class="admin-totals">
|
||||
<div><dt>Subtotal</dt><dd><?= money($order['subtotal_cents']) ?></dd></div>
|
||||
<div><dt>Tax</dt><dd><?= money($order['tax_cents']) ?></dd></div>
|
||||
<div><dt>Total</dt><dd><?= money($order['total_cents']) ?></dd></div>
|
||||
</dl>
|
||||
<?php if ($order['special_instructions']): ?>
|
||||
<div class="notice"><strong>Customer note:</strong> <?= e($order['special_instructions']) ?></div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><h2>Activity</h2></div>
|
||||
<div class="timeline">
|
||||
<?php foreach ($events as $event): ?>
|
||||
<div>
|
||||
<i></i>
|
||||
<span>
|
||||
<strong><?= e(ucwords(str_replace('_', ' ', (string) $event['event_type']))) ?></strong>
|
||||
<p><?= e($event['details']) ?></p>
|
||||
<small><?= e($event['staff_name'] ?: 'System') ?> · <?= e(store_datetime((string) $event['created_at'], 'M j, g:i A')) ?></small>
|
||||
</span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<aside>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><h2>Update Order</h2></div>
|
||||
<form class="form-stack" action="/admin/order" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="order_id" value="<?= (int) $order['id'] ?>">
|
||||
<label>Status
|
||||
<select name="status">
|
||||
<?php foreach (['paid', 'preparing', 'ready', 'completed', 'cancelled'] as $status): ?>
|
||||
<option value="<?= $status ?>" <?= selected($order['status'], $status) ?>><?= e(ucfirst($status)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label>Staff note
|
||||
<textarea name="staff_note" rows="3" placeholder="Optional internal note"></textarea>
|
||||
</label>
|
||||
<button class="button button-block" type="submit">Update Status</button>
|
||||
</form>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><h2>Customer</h2></div>
|
||||
<div class="customer-card">
|
||||
<strong><?= e($order['customer_name']) ?></strong>
|
||||
<a href="mailto:<?= e($order['customer_email']) ?>"><?= e($order['customer_email']) ?></a>
|
||||
<a href="tel:<?= e(preg_replace('/[^0-9+]/', '', (string) $order['customer_phone'])) ?>"><?= e($order['customer_phone']) ?></a>
|
||||
<span>Pickup<?= $order['pickup_time'] ? ' at ' . e($order['pickup_time']) : '' ?></span>
|
||||
</div>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><h2>Issue Refund</h2></div>
|
||||
<form class="form-stack" action="/admin/order/refund" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="order_id" value="<?= (int) $order['id'] ?>">
|
||||
<label>Amount
|
||||
<input type="number" name="amount" min="0.01" max="<?= number_format((int) $order['total_cents'] / 100, 2, '.', '') ?>" step="0.01" required>
|
||||
</label>
|
||||
<label>Reason
|
||||
<input type="text" name="reason" required placeholder="Customer request, unavailable item...">
|
||||
</label>
|
||||
<button class="button button-danger button-block" type="submit" data-confirm="Issue this refund? This action sends money back through the payment provider.">Issue Refund</button>
|
||||
</form>
|
||||
<?php if ($refunds): ?>
|
||||
<div class="refund-list">
|
||||
<?php foreach ($refunds as $refund): ?>
|
||||
<div><strong>-<?= money($refund['amount_cents']) ?></strong><span><?= e($refund['reason']) ?></span><small><?= e($refund['staff_name'] ?? 'Staff') ?></small></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading admin-toolbar">
|
||||
<form class="admin-filters" action="/admin/orders" method="get">
|
||||
<input type="search" name="q" value="<?= e($query) ?>" placeholder="Order, customer, or email">
|
||||
<select name="status">
|
||||
<option value="">All statuses</option>
|
||||
<?php foreach (['paid', 'preparing', 'ready', 'completed', 'cancelled'] as $status): ?>
|
||||
<option value="<?= $status ?>" <?= selected($statusFilter, $status) ?>><?= e(ucfirst($status)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="button button-small" type="submit">Filter</button>
|
||||
</form>
|
||||
<a class="button button-small button-ghost" href="/admin/export/orders.csv">Export CSV</a>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Order</th><th>Customer</th><th>Placed</th><th>Fulfillment</th><th>Payment</th><th>Total</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!$orders): ?><tr><td colspan="7" class="empty-cell">No orders match those filters.</td></tr><?php endif; ?>
|
||||
<?php foreach ($orders as $order): ?>
|
||||
<tr>
|
||||
<td><strong><?= e($order['order_number']) ?></strong></td>
|
||||
<td><?= e($order['customer_name']) ?><small><?= e($order['customer_email']) ?></small></td>
|
||||
<td><?= e(store_datetime((string) $order['placed_at'], 'M j, g:i A')) ?></td>
|
||||
<td><span class="status-pill status-<?= e($order['status']) ?>"><?= e(ucfirst((string) $order['status'])) ?></span></td>
|
||||
<td>
|
||||
<span class="status-pill status-<?= e($order['payment_status']) ?>"><?= e(ucwords(str_replace('_', ' ', (string) $order['payment_status']))) ?></span>
|
||||
<?php if ((int) $order['refunded_cents'] > 0): ?><small><?= money($order['refunded_cents']) ?> returned</small><?php endif; ?>
|
||||
</td>
|
||||
<td><?= money($order['total_cents']) ?></td>
|
||||
<td><a class="button button-small button-ghost" href="/admin/order?id=<?= (int) $order['id'] ?>">Open</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,116 @@
|
||||
<form class="admin-stack" action="/admin/settings" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">Public identity</span><h2>Business Information</h2></div></div>
|
||||
<div class="form-grid form-grid-three">
|
||||
<label>Public site URL<input type="url" name="app[url]" required value="<?= e($config->get('app.url')) ?>"></label>
|
||||
<label>Business name<input type="text" name="business[name]" required value="<?= e($config->get('business.name')) ?>"></label>
|
||||
<label>Phone<input type="tel" name="business[phone]" value="<?= e($config->get('business.phone')) ?>"></label>
|
||||
<label>Order email<input type="email" name="business[email]" value="<?= e($config->get('business.email')) ?>"></label>
|
||||
<label>Street<input type="text" name="business[street]" value="<?= e($config->get('business.street')) ?>"></label>
|
||||
<label>City<input type="text" name="business[city]" value="<?= e($config->get('business.city')) ?>"></label>
|
||||
<label>State<input type="text" name="business[state]" value="<?= e($config->get('business.state')) ?>"></label>
|
||||
<label>ZIP code<input type="text" name="business[postal_code]" value="<?= e($config->get('business.postal_code')) ?>"></label>
|
||||
<label>Published hours<input type="text" name="business[hours]" value="<?= e($config->get('business.hours')) ?>"></label>
|
||||
<label class="field-wide">Announcement bar<input type="text" name="business[announcement]" value="<?= e($config->get('business.announcement')) ?>"></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">Kitchen controls</span><h2>Online Ordering</h2></div></div>
|
||||
<div class="settings-row">
|
||||
<label class="check-control"><input type="checkbox" name="ordering[accepting_orders]" value="1" <?= checked($config->get('ordering.accepting_orders')) ?>><span><strong>Accept online orders</strong><small>Turn this off to pause checkout without hiding the menu.</small></span></label>
|
||||
<label>Estimated pickup time, minutes<input type="number" min="5" max="180" name="ordering[pickup_eta_minutes]" value="<?= (int) $config->get('ordering.pickup_eta_minutes') ?>"></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">Card processing</span><h2>Square</h2></div><span class="status-dot <?= $config->get('square.enabled') ? 'is-on' : '' ?>"><?= $config->get('square.enabled') ? 'Enabled' : 'Demo mode' ?></span></div>
|
||||
<div class="notice">When disabled, checkout runs in demo mode and never charges a card. Enable only after entering matching Square application, location, and access credentials.</div>
|
||||
<div class="form-grid">
|
||||
<label>Environment<select name="square[environment]"><option value="sandbox" <?= selected($config->get('square.environment'), 'sandbox') ?>>Sandbox</option><option value="production" <?= selected($config->get('square.environment'), 'production') ?>>Production</option></select></label>
|
||||
<label>Application ID<input type="text" name="square[application_id]" value="<?= e($config->get('square.application_id')) ?>" autocomplete="off"></label>
|
||||
<label>Location ID<input type="text" name="square[location_id]" value="<?= e($config->get('square.location_id')) ?>" autocomplete="off"></label>
|
||||
<label>Access token<input type="password" name="square[access_token]" value="" placeholder="<?= $config->get('square.access_token') ? 'Saved credential - leave blank to keep' : 'Enter access token' ?>" autocomplete="new-password"></label>
|
||||
</div>
|
||||
<label class="check-control"><input type="checkbox" name="square[enabled]" value="1" <?= checked($config->get('square.enabled')) ?>><span><strong>Enable Square payments</strong><small>Live checkout will require a valid card token.</small></span></label>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">Transactional email</span><h2>Mailgun</h2></div><span class="status-dot <?= $config->get('mailgun.enabled') ? 'is-on' : '' ?>"><?= $config->get('mailgun.enabled') ? 'Enabled' : 'Local log mode' ?></span></div>
|
||||
<div class="notice">When disabled, verification and newsletter messages are written to <code>storage/logs/mail.log</code> for local testing.</div>
|
||||
<div class="form-grid">
|
||||
<label>Mailgun domain<input type="text" name="mailgun[domain]" value="<?= e($config->get('mailgun.domain')) ?>"></label>
|
||||
<label>API key<input type="password" name="mailgun[api_key]" value="" placeholder="<?= $config->get('mailgun.api_key') ? 'Saved credential - leave blank to keep' : 'Enter API key' ?>" autocomplete="new-password"></label>
|
||||
<label>From name<input type="text" name="mailgun[from_name]" value="<?= e($config->get('mailgun.from_name')) ?>"></label>
|
||||
<label>From email<input type="email" name="mailgun[from_email]" value="<?= e($config->get('mailgun.from_email')) ?>"></label>
|
||||
</div>
|
||||
<label class="check-control"><input type="checkbox" name="mailgun[enabled]" value="1" <?= checked($config->get('mailgun.enabled')) ?>><span><strong>Enable Mailgun delivery</strong></span></label>
|
||||
</section>
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">Optional database server</span><h2>MySQL / MariaDB Connection</h2></div></div>
|
||||
<div class="notice">These values are only used by the migration tool below. The application continues using SQLite unless a migration succeeds and you explicitly switch drivers.</div>
|
||||
<div class="form-grid form-grid-three">
|
||||
<label>Host<input type="text" name="database[mysql_host]" value="<?= e($config->get('database.mysql_host')) ?>"></label>
|
||||
<label>Port<input type="number" min="1" max="65535" name="database[mysql_port]" value="<?= (int) $config->get('database.mysql_port') ?>"></label>
|
||||
<label>Database<input type="text" name="database[mysql_database]" value="<?= e($config->get('database.mysql_database')) ?>"></label>
|
||||
<label>Username<input type="text" name="database[mysql_username]" value="<?= e($config->get('database.mysql_username')) ?>" autocomplete="off"></label>
|
||||
<label>Password<input type="password" name="database[mysql_password]" value="" placeholder="<?= $config->get('database.mysql_password') ? 'Saved credential - leave blank to keep' : 'Enter database password' ?>" autocomplete="new-password"></label>
|
||||
</div>
|
||||
</section>
|
||||
<div class="sticky-save"><button class="button button-large" type="submit">Save Store Settings</button></div>
|
||||
</form>
|
||||
|
||||
<section class="admin-panel settings-followup">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">State and city</span><h2>Tax Rates</h2></div></div>
|
||||
<div class="editable-list">
|
||||
<?php foreach ($taxRates as $tax): ?>
|
||||
<details>
|
||||
<summary>
|
||||
<span class="item-avatar">%</span>
|
||||
<span><strong><?= e($tax['name']) ?></strong><small><?= e(ucfirst((string) $tax['jurisdiction'])) ?> jurisdiction</small></span>
|
||||
<strong><?= number_format((int) $tax['rate_basis_points'] / 100, 2) ?>%</strong>
|
||||
<span class="status-dot <?= $tax['active'] ? 'is-on' : '' ?>"><?= $tax['active'] ? 'Active' : 'Disabled' ?></span>
|
||||
<span class="edit-label">Edit</span>
|
||||
</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/tax/save" method="post">
|
||||
<?= csrf_field() ?><input type="hidden" name="id" value="<?= (int) $tax['id'] ?>">
|
||||
<div class="form-grid">
|
||||
<label>Name<input type="text" name="name" value="<?= e($tax['name']) ?>" required></label>
|
||||
<label>Jurisdiction<select name="jurisdiction"><option value="state" <?= selected($tax['jurisdiction'], 'state') ?>>State</option><option value="city" <?= selected($tax['jurisdiction'], 'city') ?>>City</option><option value="county" <?= selected($tax['jurisdiction'], 'county') ?>>County</option></select></label>
|
||||
<label>Rate, percent<input type="number" min="0" step="0.01" name="rate" value="<?= number_format((int) $tax['rate_basis_points'] / 100, 2, '.', '') ?>"></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" <?= checked($tax['active']) ?>><span><strong>Active</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Save Tax</button>
|
||||
</form>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<details class="new-record">
|
||||
<summary>+ Add Tax Rate</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/tax/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="form-grid">
|
||||
<label>Name<input type="text" name="name" required></label>
|
||||
<label>Jurisdiction<select name="jurisdiction"><option value="state">State</option><option value="city">City</option><option value="county">County</option></select></label>
|
||||
<label>Rate, percent<input type="number" min="0" step="0.01" name="rate" required></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" checked><span><strong>Active</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Create Tax Rate</button>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="admin-panel settings-followup">
|
||||
<div class="admin-panel-heading"><div><span class="eyebrow">Optional database mode</span><h2>MySQL / MariaDB Migration</h2></div><span class="role-badge"><?= e(strtoupper((string) $config->get('database.driver', 'sqlite'))) ?> active</span></div>
|
||||
<p class="muted">SQLite remains the default and requires no database server. This tool copies every application table and row into MySQL or MariaDB; switching drivers is optional.</p>
|
||||
<form class="form-stack" action="/admin/migrate/mysql" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="form-grid form-grid-three">
|
||||
<label>Host<input type="text" value="<?= e($config->get('database.mysql_host')) ?>" disabled></label>
|
||||
<label>Port<input type="text" value="<?= e($config->get('database.mysql_port')) ?>" disabled></label>
|
||||
<label>Database<input type="text" value="<?= e($config->get('database.mysql_database')) ?>" disabled></label>
|
||||
<label>Username<input type="text" value="<?= e($config->get('database.mysql_username')) ?>" disabled></label>
|
||||
</div>
|
||||
<p class="form-footnote">Connection values are saved in the Store Settings form above before running migration.</p>
|
||||
<label class="check-control"><input type="checkbox" name="switch_driver" value="1"><span><strong>Switch the application to MySQL after a successful copy</strong><small>Do this only when the MySQL service will remain available.</small></span></label>
|
||||
<button class="button button-outline" type="submit" data-confirm="Copy the SQLite database to the configured MySQL server?">Run Database Migration</button>
|
||||
</form>
|
||||
</section>
|
||||
@@ -0,0 +1,63 @@
|
||||
<div class="admin-page-actions">
|
||||
<p>All active specials are automatically arranged on the homepage based on how many are running.</p>
|
||||
</div>
|
||||
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading">
|
||||
<div><span class="eyebrow">Homepage promotion</span><h2>Specials</h2></div>
|
||||
<span class="count-badge"><?= count($specials) ?> configured</span>
|
||||
</div>
|
||||
<div class="special-admin-grid">
|
||||
<?php foreach ($specials as $special): ?>
|
||||
<details class="special-admin-card">
|
||||
<summary>
|
||||
<span class="special-badge"><?= e($special['badge']) ?></span>
|
||||
<h3><?= e($special['title']) ?></h3>
|
||||
<p><?= e($special['description']) ?></p>
|
||||
<div><strong><?= $special['price_cents'] !== null ? money($special['price_cents']) : 'No override' ?></strong><span class="status-dot <?= $special['active'] ? 'is-on' : '' ?>"><?= $special['active'] ? 'Active' : 'Hidden' ?></span></div>
|
||||
</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/special/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int) $special['id'] ?>">
|
||||
<div class="form-grid">
|
||||
<label>Title<input type="text" name="title" required value="<?= e($special['title']) ?>"></label>
|
||||
<label>Badge<input type="text" name="badge" value="<?= e($special['badge']) ?>"></label>
|
||||
<label>Linked menu item
|
||||
<select name="menu_item_id">
|
||||
<option value="">No linked item</option>
|
||||
<?php foreach ($items as $item): ?><option value="<?= (int) $item['id'] ?>" <?= selected($special['menu_item_id'], $item['id']) ?>><?= e($item['name']) ?></option><?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label>Promotional price<input type="number" name="price" min="0" step="0.01" value="<?= $special['price_cents'] !== null ? number_format((int) $special['price_cents'] / 100, 2, '.', '') : '' ?>"></label>
|
||||
<label class="field-wide">Description<textarea name="description" rows="2"><?= e($special['description']) ?></textarea></label>
|
||||
<label>Starts at<input type="datetime-local" name="starts_at" value="<?= e(store_datetime($special['starts_at'], 'Y-m-d\TH:i')) ?>"></label>
|
||||
<label>Ends at<input type="datetime-local" name="ends_at" value="<?= e(store_datetime($special['ends_at'], 'Y-m-d\TH:i')) ?>"></label>
|
||||
<label>Display order<input type="number" name="display_order" value="<?= (int) $special['display_order'] ?>"></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" <?= checked($special['active']) ?>><span><strong>Active</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Save Special</button>
|
||||
</form>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<details class="new-record">
|
||||
<summary>+ Add Homepage Special</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/special/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="form-grid">
|
||||
<label>Title<input type="text" name="title" required></label>
|
||||
<label>Badge<input type="text" name="badge" value="Special"></label>
|
||||
<label>Linked menu item
|
||||
<select name="menu_item_id"><option value="">No linked item</option><?php foreach ($items as $item): ?><option value="<?= (int) $item['id'] ?>"><?= e($item['name']) ?></option><?php endforeach; ?></select>
|
||||
</label>
|
||||
<label>Promotional price<input type="number" name="price" min="0" step="0.01"></label>
|
||||
<label class="field-wide">Description<textarea name="description" rows="2"></textarea></label>
|
||||
<label>Starts at<input type="datetime-local" name="starts_at"></label>
|
||||
<label>Ends at<input type="datetime-local" name="ends_at"></label>
|
||||
<label>Display order<input type="number" name="display_order" value="0"></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" checked><span><strong>Active</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Create Special</button>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
@@ -0,0 +1,65 @@
|
||||
<div class="admin-page-actions">
|
||||
<form class="admin-filters" action="/admin/users" method="get">
|
||||
<input type="search" name="q" value="<?= e($query) ?>" placeholder="Search name, email, or phone">
|
||||
<button class="button button-small" type="submit">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<section class="admin-panel">
|
||||
<div class="admin-panel-heading">
|
||||
<div><span class="eyebrow">Access and roster</span><h2>Users & Staff</h2></div>
|
||||
<span class="count-badge"><?= count($users) ?> shown</span>
|
||||
</div>
|
||||
<div class="editable-list">
|
||||
<?php foreach ($users as $user): ?>
|
||||
<details>
|
||||
<summary>
|
||||
<span class="avatar"><?= e(strtoupper(substr((string) $user['full_name'], 0, 1))) ?></span>
|
||||
<span><strong><?= e($user['full_name']) ?></strong><small><?= e($user['email']) ?> · <?= e($user['phone']) ?></small></span>
|
||||
<span class="role-badge"><?= e(ucfirst((string) $user['role'])) ?></span>
|
||||
<span class="status-dot <?= $user['active'] ? 'is-on' : '' ?>"><?= $user['active'] ? 'Active' : 'Disabled' ?></span>
|
||||
<span class="edit-label">Edit</span>
|
||||
</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/user/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int) $user['id'] ?>">
|
||||
<div class="form-grid">
|
||||
<label>Full name<input type="text" name="full_name" required value="<?= e($user['full_name']) ?>"></label>
|
||||
<label>Phone<input type="tel" name="phone" value="<?= e($user['phone']) ?>"></label>
|
||||
<label>Role
|
||||
<select name="role">
|
||||
<?php foreach (['customer', 'staff', 'manager', 'admin'] as $role): ?><option value="<?= $role ?>" <?= selected($user['role'], $role) ?>><?= e(ucfirst($role)) ?></option><?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="check-row">
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" <?= checked($user['active']) ?>><span><strong>Active account</strong></span></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="newsletter_opt_in" value="1" <?= checked($user['newsletter_opt_in']) ?>><span><strong>News opted in</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Save User</button>
|
||||
</form>
|
||||
</details>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<details class="new-record">
|
||||
<summary>+ Add Staff Member</summary>
|
||||
<form class="inline-editor form-stack" action="/admin/user/save" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="form-grid form-grid-three">
|
||||
<label>Full name<input type="text" name="full_name" required></label>
|
||||
<label>Email<input type="email" name="email" required></label>
|
||||
<label>Phone<input type="tel" name="phone"></label>
|
||||
<label>Temporary password<input type="password" name="password" required minlength="10"></label>
|
||||
<label>Role
|
||||
<select name="role"><option value="staff">Staff</option><option value="manager">Manager</option><option value="admin">Administrator</option><option value="customer">Customer</option></select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="check-row">
|
||||
<label class="check-control compact"><input type="checkbox" name="active" value="1" checked><span><strong>Active account</strong></span></label>
|
||||
<label class="check-control compact"><input type="checkbox" name="newsletter_opt_in" value="1"><span><strong>News opted in</strong></span></label>
|
||||
</div>
|
||||
<button class="button button-small" type="submit">Create User</button>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user