diff --git a/admin/index.php b/admin/index.php
new file mode 100644
index 0000000..84a369d
--- /dev/null
+++ b/admin/index.php
@@ -0,0 +1,361 @@
+ isset($_POST['is_active']) ? 1 : 0,
+ 'type' => $_POST['type'] ?? 'holiday',
+ 'headline' => $_POST['headline'] ?? '',
+ 'message' => $_POST['message'] ?? '',
+ 'start_date' => $_POST['start_date'] ?? null,
+ 'end_date' => $_POST['end_date'] ?? null,
+ 'show_banner' => isset($_POST['show_banner']) ? 1 : 0,
+ 'show_in_hours' => isset($_POST['show_in_hours']) ? 1 : 0,
+ ], $annId);
+
+ $successMsg = 'Announcement updated successfully!';
+ }
+}
+
+// Fetch current announcement (or default empty template)
+$announcement = get_active_announcement();
+if (!$announcement) {
+ $stmt = $pdo->query("SELECT * FROM announcements ORDER BY id DESC LIMIT 1");
+ $announcement = $stmt->fetch() ?: [
+ 'id' => 0,
+ 'is_active' => 0,
+ 'type' => 'holiday',
+ 'headline' => 'Holiday Closure',
+ 'message' => 'The Old Fitz will be closed for the holiday weekend.',
+ 'start_date' => '',
+ 'end_date' => '',
+ 'show_banner' => 1,
+ 'show_in_hours' => 1
+ ];
+}
+?>
+
+
+
+
+
+ Pub Management Dashboard | The Old Fitz
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($successMsg) ?>
+
+
+
= htmlspecialchars($errorMsg) ?>
+
+
+
+
+
+
+
+
diff --git a/admin/login.php b/admin/login.php
new file mode 100644
index 0000000..f36619e
--- /dev/null
+++ b/admin/login.php
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+ Admin Login | The Old Fitz Public House
+
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($error) ?>
+
+
+
+
+
← Return to Pub Website
+
+
+
diff --git a/admin/logout.php b/admin/logout.php
new file mode 100644
index 0000000..72bf39d
--- /dev/null
+++ b/admin/logout.php
@@ -0,0 +1,5 @@
+= :today2)
+ ORDER BY id DESC
+ LIMIT 1
+ ";
+
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([
+ ':today1' => $today,
+ ':today2' => $today
+ ]);
+ $result = $stmt->fetch();
+
+ return $result ?: null;
+}
+
+function get_all_announcements(): array {
+ $pdo = get_db();
+ $stmt = $pdo->query("SELECT * FROM announcements ORDER BY id DESC");
+ return $stmt->fetchAll();
+}
+
+function save_announcement(array $data, ?int $id = null): int {
+ $pdo = get_db();
+
+ $isActive = !empty($data['is_active']) ? 1 : 0;
+ $type = in_array($data['type'] ?? '', ['holiday', 'special', 'closure']) ? $data['type'] : 'holiday';
+ $headline = trim($data['headline'] ?? '');
+ $message = trim($data['message'] ?? '');
+ $startDate = !empty($data['start_date']) ? trim($data['start_date']) : null;
+ $endDate = !empty($data['end_date']) ? trim($data['end_date']) : null;
+ $showBanner = !empty($data['show_banner']) ? 1 : 0;
+ $showInHours = !empty($data['show_in_hours']) ? 1 : 0;
+
+ if ($id) {
+ $stmt = $pdo->prepare("
+ UPDATE announcements SET
+ is_active = ?,
+ type = ?,
+ headline = ?,
+ message = ?,
+ start_date = ?,
+ end_date = ?,
+ show_banner = ?,
+ show_in_hours = ?,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE id = ?
+ ");
+ $stmt->execute([
+ $isActive, $type, $headline, $message, $startDate, $endDate,
+ $showBanner, $showInHours, $id
+ ]);
+ return $id;
+ } else {
+ $stmt = $pdo->prepare("
+ INSERT INTO announcements (
+ is_active, type, headline, message, start_date, end_date, show_banner, show_in_hours
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ ");
+ $stmt->execute([
+ $isActive, $type, $headline, $message, $startDate, $endDate,
+ $showBanner, $showInHours
+ ]);
+ return (int)$pdo->lastInsertId();
+ }
+}
diff --git a/inc/auth.php b/inc/auth.php
new file mode 100644
index 0000000..9e06ec9
--- /dev/null
+++ b/inc/auth.php
@@ -0,0 +1,82 @@
+ 86400 * 7, // 7 days
+ 'path' => '/',
+ 'httponly' => true,
+ 'samesite' => 'Lax'
+ ]);
+ session_start();
+}
+
+require_once __DIR__ . '/db.php';
+
+function is_authenticated(): bool {
+ return !empty($_SESSION['admin_user_id']);
+}
+
+function require_auth(): void {
+ if (!is_authenticated()) {
+ header('Location: login.php');
+ exit;
+ }
+}
+
+function get_logged_in_user(): ?array {
+ if (!is_authenticated()) {
+ return null;
+ }
+ return [
+ 'id' => $_SESSION['admin_user_id'],
+ 'username' => $_SESSION['admin_username'] ?? 'admin',
+ 'role' => $_SESSION['admin_role'] ?? 'admin'
+ ];
+}
+
+function authenticate_user(string $username, string $password): bool {
+ $pdo = get_db();
+ $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
+ $stmt->execute([trim($username)]);
+ $user = $stmt->fetch();
+
+ if ($user && password_verify($password, $user['password_hash'])) {
+ session_regenerate_id(true);
+ $_SESSION['admin_user_id'] = $user['id'];
+ $_SESSION['admin_username'] = $user['username'];
+ $_SESSION['admin_role'] = $user['role'];
+ return true;
+ }
+
+ return false;
+}
+
+function logout_user(): void {
+ $_SESSION = [];
+ if (ini_get("session.use_cookies")) {
+ $params = session_get_cookie_params();
+ setcookie(session_name(), '', time() - 42000,
+ $params["path"], $params["domain"],
+ $params["secure"], $params["httponly"]
+ );
+ }
+ session_destroy();
+}
+
+function get_csrf_token(): string {
+ if (empty($_SESSION['csrf_token'])) {
+ $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
+ }
+ return $_SESSION['csrf_token'];
+}
+
+function verify_csrf_token(?string $token): bool {
+ if (empty($_SESSION['csrf_token']) || empty($token)) {
+ return false;
+ }
+ return hash_equals($_SESSION['csrf_token'], $token);
+}
diff --git a/inc/db.php b/inc/db.php
new file mode 100644
index 0000000..9a8ca36
--- /dev/null
+++ b/inc/db.php
@@ -0,0 +1,82 @@
+ PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ PDO::ATTR_TIMEOUT => 5
+ ]);
+
+ // Enable WAL mode for concurrency and performance
+ $pdo->exec('PRAGMA journal_mode = WAL;');
+ $pdo->exec('PRAGMA foreign_keys = ON;');
+
+ // Initialize tables if needed
+ init_db_schema($pdo);
+
+ return $pdo;
+}
+
+function init_db_schema(PDO $pdo): void {
+ $pdo->exec("
+ CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ username TEXT UNIQUE NOT NULL,
+ password_hash TEXT NOT NULL,
+ role TEXT NOT NULL DEFAULT 'admin',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+
+ CREATE TABLE IF NOT EXISTS announcements (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ is_active INTEGER NOT NULL DEFAULT 1,
+ type TEXT NOT NULL DEFAULT 'holiday',
+ headline TEXT NOT NULL,
+ message TEXT NOT NULL,
+ start_date TEXT NULL,
+ end_date TEXT NULL,
+ show_banner INTEGER NOT NULL DEFAULT 1,
+ show_in_hours INTEGER NOT NULL DEFAULT 1,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+ ");
+
+ // Seed default admin user if table is empty (username: admin / password: password)
+ $stmt = $pdo->query("SELECT COUNT(*) as count FROM users");
+ $userCount = $stmt->fetchColumn();
+ if ($userCount == 0) {
+ $defaultPassHash = password_hash('password', PASSWORD_DEFAULT);
+ $insert = $pdo->prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, 'admin')");
+ $insert->execute(['admin', $defaultPassHash]);
+ }
+
+ // Seed default Labor Day announcement row if table is empty
+ $stmtAnn = $pdo->query("SELECT COUNT(*) as count FROM announcements");
+ $annCount = $stmtAnn->fetchColumn();
+ if ($annCount == 0) {
+ $insertAnn = $pdo->prepare("
+ INSERT INTO announcements (
+ is_active, type, headline, message, start_date, end_date, show_banner, show_in_hours
+ ) VALUES (
+ 1, 'holiday', 'Labor Day Closure', 'The Old Fitz will be closed Thursday, Sept 3 through Saturday, Sept 5 for the holiday weekend. Doors reopen Thursday, Sept 10 at 4:00 PM!', NULL, NULL, 1, 1
+ )
+ ");
+ $insertAnn->execute();
+ }
+}