- 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:
Ty Clifford
2026-06-10 13:27:42 -04:00
parent d46b0be488
commit 16235369cb
49 changed files with 7685 additions and 2 deletions
+98
View File
@@ -0,0 +1,98 @@
<section class="page-hero page-hero-compact">
<div class="container">
<span class="eyebrow">Secure checkout</span>
<h1>Finish Your Order</h1>
</div>
</section>
<div class="container checkout-layout section">
<section class="checkout-main">
<form id="checkout-form" class="panel form-stack"
action="/checkout" method="post"
data-square-enabled="<?= $squareEnabled ? 'true' : 'false' ?>"
data-square-app-id="<?= e($squareAppId) ?>"
data-square-location-id="<?= e($squareLocationId) ?>">
<?= csrf_field() ?>
<input type="hidden" id="square-token" name="square_token" value="">
<div class="form-section">
<div class="form-section-heading">
<span>1</span>
<div>
<h2>Who is picking up?</h2>
<p>We will use this to confirm your order.</p>
</div>
</div>
<div class="form-grid">
<label>Full name
<input type="text" name="customer_name" required autocomplete="name"
value="<?= e($old['customer_name'] ?? $currentUser['full_name'] ?? '') ?>">
</label>
<label>Email address
<input type="email" name="customer_email" required autocomplete="email"
value="<?= e($old['customer_email'] ?? $currentUser['email'] ?? '') ?>">
</label>
<label>Phone number
<input type="tel" name="customer_phone" required autocomplete="tel"
value="<?= e($old['customer_phone'] ?? $currentUser['phone'] ?? '') ?>">
</label>
<label>Preferred pickup time
<input type="time" name="pickup_time" value="<?= e($old['pickup_time'] ?? '') ?>">
</label>
</div>
<label>Special instructions
<textarea name="special_instructions" rows="3" placeholder="Sauce on the side, allergy note, pickup details..."><?= e($old['special_instructions'] ?? '') ?></textarea>
</label>
</div>
<div class="form-section">
<div class="form-section-heading">
<span>2</span>
<div>
<h2>Payment</h2>
<p><?= $squareEnabled ? 'Securely processed by Square.' : 'Demo payment mode is active until Square is enabled in Settings.' ?></p>
</div>
</div>
<?php if ($squareEnabled): ?>
<?php if ($squareAppId && $squareLocationId): ?>
<div id="card-container" class="square-card"></div>
<div id="card-errors" class="field-error" role="alert"></div>
<?php else: ?>
<div class="notice notice-error">Square is enabled, but the application or location ID is missing. Ask an administrator to finish the payment settings.</div>
<?php endif; ?>
<?php else: ?>
<div class="demo-payment">
<span class="demo-payment-mark">D</span>
<div>
<strong>Demo payment</strong>
<p>No card will be charged. Orders are recorded as paid for local testing.</p>
</div>
</div>
<?php endif; ?>
</div>
<button id="checkout-submit" class="button button-large button-block" type="submit"
<?= $squareEnabled && (!$squareAppId || !$squareLocationId) ? 'disabled' : '' ?>>
Pay <?= money($totals['total_cents']) ?> & Place Order
</button>
</form>
</section>
<aside class="order-summary">
<span class="eyebrow">Your order</span>
<div class="mini-order-list">
<?php foreach ($cart['items'] as $item): ?>
<div>
<span><b><?= (int) $item['quantity'] ?>x</b> <?= e($item['name']) ?></span>
<strong><?= money((int) $item['quantity'] * (int) $item['unit_price_cents']) ?></strong>
</div>
<?php endforeach; ?>
</div>
<dl>
<div><dt>Subtotal</dt><dd><?= money($totals['subtotal_cents']) ?></dd></div>
<?php foreach ($taxRates as $tax): ?>
<div><dt><?= e($tax['name']) ?> (<?= number_format((int) $tax['rate_basis_points'] / 100, 2) ?>%)</dt><dd>Included below</dd></div>
<?php endforeach; ?>
<div><dt>Tax</dt><dd><?= money($totals['tax_cents']) ?></dd></div>
<div class="summary-total"><dt>Total</dt><dd><?= money($totals['total_cents']) ?></dd></div>
</dl>
<p class="summary-note">Estimated ready time: <?= (int) $config->get('ordering.pickup_eta_minutes', 25) ?> minutes.</p>
</aside>
</div>