- 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
+100
View File
@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace FatBottom\Services;
use FatBottom\Core\Database;
final class StatsService
{
public function __construct(private readonly Database $db)
{
}
public function record(string $route, ?int $userId): void
{
if (str_starts_with($route, '/assets/')) {
return;
}
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$this->db->execute(
'INSERT INTO visitor_events (session_id, user_id, route, referrer, user_agent, ip_hash)
VALUES (?, ?, ?, ?, ?, ?)',
[
session_id(),
$userId,
substr($route, 0, 255),
substr((string) ($_SERVER['HTTP_REFERER'] ?? ''), 0, 500),
substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 500),
hash('sha256', $ip . date('Y-m-d')),
]
);
}
public function dashboard(): array
{
if (!$this->db->isSqlite()) {
return [
'today_orders' => (int) $this->db->fetch(
'SELECT COUNT(*) AS value FROM orders WHERE DATE(placed_at) = CURRENT_DATE'
)['value'],
'today_sales' => (int) $this->db->fetch(
"SELECT COALESCE(SUM(o.total_cents), 0) - COALESCE(SUM(r.refunded_cents), 0) AS value
FROM orders o
LEFT JOIN (
SELECT order_id, SUM(amount_cents) AS refunded_cents FROM refunds GROUP BY order_id
) r ON r.order_id = o.id
WHERE o.payment_status IN ('paid', 'partially_refunded', 'refunded')
AND DATE(o.placed_at) = CURRENT_DATE"
)['value'],
'open_orders' => (int) $this->db->fetch(
"SELECT COUNT(*) AS value FROM orders WHERE status IN ('paid', 'preparing', 'ready')"
)['value'],
'abandoned_carts' => (int) $this->db->fetch(
"SELECT COUNT(*) AS value FROM carts
WHERE status = 'active' AND subtotal_cents > 0
AND last_seen_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE)"
)['value'],
'today_visitors' => (int) $this->db->fetch(
'SELECT COUNT(DISTINCT session_id) AS value FROM visitor_events
WHERE DATE(created_at) = CURRENT_DATE'
)['value'],
'newsletter_members' => (int) $this->db->fetch(
'SELECT COUNT(*) AS value FROM users WHERE newsletter_opt_in = 1 AND active = 1'
)['value'],
];
}
return [
'today_orders' => (int) $this->db->fetch(
"SELECT COUNT(*) AS value FROM orders
WHERE date(placed_at, 'localtime') = date('now', 'localtime')"
)['value'],
'today_sales' => (int) $this->db->fetch(
"SELECT COALESCE(SUM(o.total_cents), 0) - COALESCE(SUM(r.refunded_cents), 0) AS value
FROM orders o
LEFT JOIN (
SELECT order_id, SUM(amount_cents) AS refunded_cents FROM refunds GROUP BY order_id
) r ON r.order_id = o.id
WHERE o.payment_status IN ('paid', 'partially_refunded', 'refunded')
AND date(o.placed_at, 'localtime') = date('now', 'localtime')"
)['value'],
'open_orders' => (int) $this->db->fetch(
"SELECT COUNT(*) AS value FROM orders WHERE status IN ('paid', 'preparing', 'ready')"
)['value'],
'abandoned_carts' => (int) $this->db->fetch(
"SELECT COUNT(*) AS value FROM carts
WHERE status = 'active' AND subtotal_cents > 0
AND last_seen_at < datetime('now', '-30 minutes')"
)['value'],
'today_visitors' => (int) $this->db->fetch(
"SELECT COUNT(DISTINCT session_id) AS value FROM visitor_events
WHERE date(created_at, 'localtime') = date('now', 'localtime')"
)['value'],
'newsletter_members' => (int) $this->db->fetch(
'SELECT COUNT(*) AS value FROM users WHERE newsletter_opt_in = 1 AND active = 1'
)['value'],
];
}
}