diff --git a/admin.php b/admin.php index ab5fac3..a23d3e7 100644 --- a/admin.php +++ b/admin.php @@ -1,53 +1,136 @@ format('M j, Y • g:i A'); + } catch (Exception $e) { + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + } +} + +function bind_named_values(PDOStatement $stmt, array $params): void { + foreach ($params as $name => $value) { + $stmt->bindValue($name, $value, is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR); + } +} + $message = ''; if (isset($_SESSION['message'])) { - $message = $_SESSION['message']; + $message = (string) $_SESSION['message']; unset($_SESSION['message']); } $pdo = db(); +$records_per_page = 15; +$page = isset($_GET['page']) && is_numeric($_GET['page']) ? max(1, (int) $_GET['page']) : 1; +$selected_webinar_id = isset($_GET['webinar_id']) && is_numeric($_GET['webinar_id']) ? max(0, (int) $_GET['webinar_id']) : 0; -// Pagination settings -$records_per_page = 10; -$page = isset($_GET['page']) && is_numeric($_GET['page']) ? (int)$_GET['page'] : 1; +$webinars = $pdo->query('SELECT id, title, scheduled_at FROM webinars ORDER BY scheduled_at DESC, id DESC')->fetchAll(PDO::FETCH_ASSOC); + +$where_parts = ['a.deleted_at IS NULL']; +$params = []; +if ($selected_webinar_id > 0) { + $where_parts[] = 'a.webinar_id = :webinar_id'; + $params[':webinar_id'] = $selected_webinar_id; +} +$where_sql = 'WHERE ' . implode(' AND ', $where_parts); + +$count_stmt = $pdo->prepare("SELECT COUNT(*) FROM attendees a {$where_sql}"); +bind_named_values($count_stmt, $params); +$count_stmt->execute(); +$total_records = (int) $count_stmt->fetchColumn(); + +$total_pages = max(1, (int) ceil($total_records / $records_per_page)); +if ($page > $total_pages) { + $page = $total_pages; +} $offset = ($page - 1) * $records_per_page; -// Get total number of records -$total_stmt = $pdo->query("SELECT COUNT(*) FROM attendees"); -$total_records = $total_stmt->fetchColumn(); -$total_pages = ceil($total_records / $records_per_page); +$today_stmt = $pdo->prepare("SELECT COUNT(*) FROM attendees a {$where_sql} AND DATE(a.created_at) = CURDATE()"); +bind_named_values($today_stmt, $params); +$today_stmt->execute(); +$today_count = (int) $today_stmt->fetchColumn(); -// Get records for the current page -$stmt = $pdo->prepare("SELECT id, first_name, last_name, email, created_at, how_did_you_hear, company FROM attendees ORDER BY first_name ASC, last_name ASC LIMIT :limit OFFSET :offset"); -$stmt->bindValue(':limit', $records_per_page, PDO::PARAM_INT); -$stmt->bindValue(':offset', $offset, PDO::PARAM_INT); -$stmt->execute(); -$attendees = $stmt->fetchAll(PDO::FETCH_ASSOC); +$last7_stmt = $pdo->prepare("SELECT COUNT(*) FROM attendees a {$where_sql} AND a.created_at >= (NOW() - INTERVAL 7 DAY)"); +bind_named_values($last7_stmt, $params); +$last7_stmt->execute(); +$last_7_days_count = (int) $last7_stmt->fetchColumn(); -// Get data for the chart -$chart_stmt = $pdo->query("SELECT DATE(created_at) as registration_day, COUNT(*) as user_count FROM attendees GROUP BY registration_day ORDER BY registration_day"); +$latest_stmt = $pdo->prepare("SELECT MAX(a.created_at) FROM attendees a {$where_sql}"); +bind_named_values($latest_stmt, $params); +$latest_stmt->execute(); +$latest_registration_at = $latest_stmt->fetchColumn(); + +$company_stmt = $pdo->prepare("SELECT COUNT(DISTINCT NULLIF(TRIM(a.company), '')) FROM attendees a {$where_sql}"); +bind_named_values($company_stmt, $params); +$company_stmt->execute(); +$unique_companies = (int) $company_stmt->fetchColumn(); + +$attendees_sql = "SELECT + a.id, + a.webinar_id, + a.first_name, + a.last_name, + a.email, + a.company, + a.how_did_you_hear, + a.timezone, + a.consented, + a.created_at, + w.title AS webinar_title, + w.scheduled_at AS webinar_scheduled_at + FROM attendees a + LEFT JOIN webinars w ON w.id = a.webinar_id + {$where_sql} + ORDER BY a.created_at DESC, a.id DESC + LIMIT :limit OFFSET :offset"; +$attendees_stmt = $pdo->prepare($attendees_sql); +bind_named_values($attendees_stmt, $params); +$attendees_stmt->bindValue(':limit', $records_per_page, PDO::PARAM_INT); +$attendees_stmt->bindValue(':offset', $offset, PDO::PARAM_INT); +$attendees_stmt->execute(); +$attendees = $attendees_stmt->fetchAll(PDO::FETCH_ASSOC); + +$chart_stmt = $pdo->prepare("SELECT DATE(a.created_at) AS registration_day, COUNT(*) AS user_count + FROM attendees a + {$where_sql} + GROUP BY DATE(a.created_at) + ORDER BY DATE(a.created_at)"); +bind_named_values($chart_stmt, $params); +$chart_stmt->execute(); $chart_data = $chart_stmt->fetchAll(PDO::FETCH_ASSOC); +$selected_webinar = null; +foreach ($webinars as $webinar) { + if ((int) $webinar['id'] === $selected_webinar_id) { + $selected_webinar = $webinar; + break; + } +} + $chart_labels = json_encode(array_column($chart_data, 'registration_day')); $chart_values = json_encode(array_column($chart_data, 'user_count')); - +$export_link = 'export_csv.php' . ($selected_webinar_id > 0 ? '?webinar_id=' . urlencode((string) $selected_webinar_id) : ''); ?> - Admin Dashboard + + Admin Dashboard | Webinar Registrations + + @@ -65,7 +148,6 @@ $chart_values = json_encode(array_column($chart_data, 'user_count')); --text-muted: #97acd0; --accent: #2c4bd1; --accent-strong: #1029a0; - --accent-soft: #bbc8fb; --success: #18d1b3; --danger: #ff6b81; --font-body: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; @@ -82,39 +164,36 @@ $chart_values = json_encode(array_column($chart_data, 'user_count')); color: var(--text-main); } .admin-shell { - max-width: 1220px; + max-width: 1380px; padding-top: 40px; padding-bottom: 40px; } - h2, h3, .h5, p, label, th, td { - color: var(--text-main); - } - h2, h3, .h5, th, .btn, .page-link { + h1, h2, h3, .h5, .btn, .page-link, th, label { font-family: var(--font-display); } - h2 { - font-size: 2rem; + h1 { + font-size: 2.2rem; font-weight: 700; - letter-spacing: -0.03em; + letter-spacing: -0.04em; margin-bottom: 0.35rem; } - h3, .h5 { - font-weight: 700; - letter-spacing: -0.02em; - } - p.text-muted, .text-muted { + .lead-copy, + .text-muted { color: var(--text-muted) !important; } .panel-card, - .table-shell { + .table-shell, + .summary-card, + .toolbar-card { background: var(--surface); border: 1px solid var(--line); border-radius: 22px; box-shadow: 0 25px 60px rgba(3, 11, 25, 0.38); backdrop-filter: blur(16px); } - .panel-card { - margin-top: 1.5rem; + .panel-card, + .summary-card, + .toolbar-card { padding: 1.5rem; } .table-shell { @@ -122,153 +201,275 @@ $chart_values = json_encode(array_column($chart_data, 'user_count')); padding: 0.35rem; overflow: hidden; } - .alert-info { - background: rgba(34, 199, 255, 0.12); - color: var(--text-main); - border: 1px solid rgba(34, 199, 255, 0.28); + .summary-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1rem; + margin-top: 1.5rem; + margin-bottom: 1.5rem; } - .btn, - .page-link { + .summary-label { + color: var(--text-soft); + font-size: 0.9rem; + margin-bottom: 0.45rem; + } + .summary-value { + font-size: 2rem; + font-weight: 800; + line-height: 1; + } + .summary-meta { + margin-top: 0.6rem; + color: var(--text-muted); + font-size: 0.9rem; + } + .toolbar-card { + display: flex; + align-items: end; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; + margin-top: 1rem; + } + .filter-form { + display: flex; + align-items: end; + gap: 0.75rem; + flex-wrap: wrap; + } + .form-select, + .form-control { + min-width: 260px; + background: rgba(221, 226, 253, 0.08); + border-color: rgba(221, 226, 253, 0.18); + color: var(--text-main); + } + .form-select:focus, + .form-control:focus { + background: rgba(221, 226, 253, 0.10); + color: var(--text-main); + border-color: rgba(44, 75, 209, 0.65); + box-shadow: 0 0 0 0.2rem rgba(187, 200, 251, 0.18); + } + .table { + --bs-table-bg: transparent; + --bs-table-color: var(--text-main); + --bs-table-striped-bg: rgba(255, 255, 255, 0.03); + --bs-table-striped-color: var(--text-main); + --bs-table-border-color: rgba(221, 226, 253, 0.12); + margin-bottom: 0; + } + th { + color: var(--text-soft); + font-size: 0.85rem; + letter-spacing: 0.04em; + text-transform: uppercase; + white-space: nowrap; + } + td { + vertical-align: middle; + } + .tag { + display: inline-flex; + align-items: center; + padding: 0.32rem 0.7rem; + border-radius: 999px; + background: rgba(34, 199, 255, 0.14); + color: var(--text-main); + font-size: 0.84rem; + border: 1px solid rgba(34, 199, 255, 0.18); + } + .attendee-name { + font-weight: 700; + } + .attendee-meta, + .webinar-meta { + display: block; + margin-top: 0.25rem; + color: var(--text-muted); + font-size: 0.86rem; + } + .btn { border-radius: 14px; font-weight: 700; letter-spacing: 0.02em; - transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; } - .btn:hover, - .page-link:hover { - transform: translateY(-1px); - } - .btn-outline-primary { - color: var(--accent-soft, #bbc8fb); - border-color: rgba(34, 199, 255, 0.35); - background: rgba(221, 226, 253, 0.06); - box-shadow: none; - } - .btn-outline-primary:hover { - background: rgba(187, 200, 251, 0.18); - border-color: rgba(34, 199, 255, 0.55); - box-shadow: var(--button-shadow); - } - .btn-success, .btn-primary { background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%); - border: 1px solid rgba(221, 226, 253, 0.12); + border: 1px solid rgba(221, 226, 253, 0.14); box-shadow: var(--button-shadow); } - .btn-success:hover, - .btn-primary:hover { - background: linear-gradient(135deg, #bbc8fb 0%, #2c4bd1 100%); - box-shadow: 0 20px 34px rgba(11, 32, 131, 0.32); + .btn-success { + background: linear-gradient(135deg, #18d1b3 0%, #0b8f7a 100%); + border-color: transparent; + box-shadow: 0 16px 30px rgba(24, 209, 179, 0.18); } - .btn-danger { - background: linear-gradient(135deg, #ff7a8b 0%, #f04f74 100%); - border: 1px solid rgba(255, 255, 255, 0.08); - box-shadow: 0 16px 30px rgba(72, 18, 36, 0.28); + .btn-outline-light { + border-color: rgba(221, 226, 253, 0.28); } - .table { - margin-bottom: 0; - --bs-table-bg: transparent; - --bs-table-striped-bg: rgba(221, 226, 253, 0.06); - --bs-table-striped-color: var(--text-main); - --bs-table-hover-bg: rgba(187, 200, 251, 0.12); - --bs-table-hover-color: var(--text-main); + .pagination .page-link { color: var(--text-main); + background: rgba(221, 226, 253, 0.05); + border-color: rgba(221, 226, 253, 0.14); } - .table thead th { - border-bottom-color: rgba(221, 226, 253, 0.18); - color: var(--accent-soft); - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.08em; - font-size: 0.78rem; - background: rgba(255, 255, 255, 0.02); - } - .table td, - .table th { - border-color: rgba(143, 232, 255, 0.08); - vertical-align: middle; - } - .page-link { - background: rgba(255, 255, 255, 0.04); - color: var(--text-main); - border-color: rgba(143, 232, 255, 0.12); - box-shadow: none; - } - .page-item.active .page-link { - background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%); + .pagination .page-item.active .page-link { + background: var(--accent); border-color: transparent; } - .page-link:hover { - color: var(--text-main); - background: rgba(34, 199, 255, 0.12); - border-color: rgba(34, 199, 255, 0.24); + .alert { + border-radius: 16px; + margin-top: 1.2rem; } - canvas { - filter: drop-shadow(0 10px 25px rgba(34, 199, 255, 0.08)); + code { + color: var(--accent-soft, #bbc8fb); + } + @media (max-width: 991px) { + .admin-shell { + padding-top: 24px; + } + .summary-value { + font-size: 1.7rem; + } + .form-select, + .form-control { + min-width: 210px; + } } -
-

Admin Dashboard

-

Welcome, !

+
+
+

Webinar registrations dashboard

+

Welcome, . Review attendee data, edit registrations, export CSV, and monitor daily signup volume.

+
-
- -
+
-
-
-

Daily Registrations

- +
+
+
+ + +
+
+ + Reset +
+
+ -
+
+ +
+
+
Total registrations
+
+
Active attendees.
+
+
+
Registered today
+
+
New signups recorded on format('M j, Y'); ?>.
+
+
+
Last 7 days
+
+
Rolling weekly registrations based on created_at.
+
+
+
Companies represented
+
+
Unique non-empty company names among active attendees.
+
+
+
Latest registration
+
+
Most recent active attendee creation timestamp.
+
+
+ +
+
+
+

Daily registrations

+

Chart based on attendee created_at, grouped by registration day.

+
+ +
+
-
+ -
-

Registered Attendees

- Download CSV -
+
+
+

Registered attendees

+

+ + Showing active records for . + + Showing active records for all webinars, newest registrations first. + +

+
+
- +
- - + - - + + + + - + - - - - - - - + + + + + + + + @@ -277,74 +478,74 @@ $chart_values = json_encode(array_column($chart_data, 'user_count'));
IDFirst NameLast NameAttendee EmailSource CompanyRegistered AtSourceTimezoneWebinarRegistered at Actions
No attendees found.No attendees found for this filter yet.
- Edit -
- - -
+ + Consent: +
+ + + +
+ Edit +
+ + +
+
- - -
+ 1): ?> + + +
- \ No newline at end of file + diff --git a/db/config.php b/db/config.php index bb98f7d..41a9a9d 100644 --- a/db/config.php +++ b/db/config.php @@ -7,11 +7,33 @@ define('DB_PASS', 'e45f2778-db1f-450c-99c6-29efb4601472'); function db() { static $pdo; + if (!$pdo) { - $pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - ]); + try { + $pdo = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4', DB_USER, DB_PASS, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]); + } catch (PDOException $e) { + if ((int) $e->getCode() !== 1049 && stripos($e->getMessage(), 'Unknown database') === false) { + throw $e; + } + + $bootstrap = new PDO('mysql:host=' . DB_HOST . ';charset=utf8mb4', DB_USER, DB_PASS, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]); + $bootstrap->exec('CREATE DATABASE IF NOT EXISTS `' . DB_NAME . '` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); + + $pdo = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4', DB_USER, DB_PASS, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]); + } + + require_once __DIR__ . '/ensure_schema.php'; + ensure_app_schema($pdo); } + return $pdo; } diff --git a/db/ensure_schema.php b/db/ensure_schema.php new file mode 100644 index 0000000..796dade --- /dev/null +++ b/db/ensure_schema.php @@ -0,0 +1,121 @@ +prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?'); + $stmt->execute([$table]); + return (int) $stmt->fetchColumn() > 0; +} + +function schema_column_exists(PDO $pdo, string $table, string $column): bool { + $stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?'); + $stmt->execute([$table, $column]); + return (int) $stmt->fetchColumn() > 0; +} + +function ensure_app_schema(PDO $pdo): void { + static $hasRun = false; + if ($hasRun) { + return; + } + $hasRun = true; + + $pdo->exec('CREATE TABLE IF NOT EXISTS migrations ( + id INT AUTO_INCREMENT PRIMARY KEY, + migration VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'); + + $pdo->exec('CREATE TABLE IF NOT EXISTS webinars ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + presenter VARCHAR(255), + scheduled_at DATETIME NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'); + + $pdo->exec('CREATE TABLE IF NOT EXISTS attendees ( + id INT AUTO_INCREMENT PRIMARY KEY, + webinar_id INT NOT NULL, + first_name VARCHAR(255) NOT NULL DEFAULT "", + last_name VARCHAR(255) NOT NULL DEFAULT "", + name VARCHAR(255) NULL, + email VARCHAR(255) NOT NULL, + company VARCHAR(255) DEFAULT NULL, + timezone VARCHAR(255) DEFAULT NULL, + how_did_you_hear VARCHAR(255) DEFAULT NULL, + password VARCHAR(255) NOT NULL DEFAULT "", + consented TINYINT(1) NOT NULL DEFAULT 0, + deleted_at TIMESTAMP NULL DEFAULT NULL, + created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_attendees_webinar FOREIGN KEY (webinar_id) REFERENCES webinars(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'); + + if (schema_table_exists($pdo, 'attendees')) { + if (schema_column_exists($pdo, 'attendees', 'name')) { + $pdo->exec('ALTER TABLE attendees MODIFY COLUMN name VARCHAR(255) NULL'); + } + + if (!schema_column_exists($pdo, 'attendees', 'first_name')) { + $pdo->exec('ALTER TABLE attendees ADD COLUMN first_name VARCHAR(255) NOT NULL DEFAULT "" AFTER webinar_id'); + } + + if (!schema_column_exists($pdo, 'attendees', 'last_name')) { + $pdo->exec('ALTER TABLE attendees ADD COLUMN last_name VARCHAR(255) NOT NULL DEFAULT "" AFTER first_name'); + } + + if (!schema_column_exists($pdo, 'attendees', 'company')) { + $pdo->exec('ALTER TABLE attendees ADD COLUMN company VARCHAR(255) DEFAULT NULL AFTER email'); + } + + if (!schema_column_exists($pdo, 'attendees', 'timezone')) { + $pdo->exec('ALTER TABLE attendees ADD COLUMN timezone VARCHAR(255) DEFAULT NULL AFTER company'); + } + + if (!schema_column_exists($pdo, 'attendees', 'how_did_you_hear')) { + $pdo->exec('ALTER TABLE attendees ADD COLUMN how_did_you_hear VARCHAR(255) DEFAULT NULL AFTER timezone'); + } + + if (!schema_column_exists($pdo, 'attendees', 'password')) { + $pdo->exec('ALTER TABLE attendees ADD COLUMN password VARCHAR(255) NOT NULL DEFAULT "" AFTER how_did_you_hear'); + } + + if (!schema_column_exists($pdo, 'attendees', 'consented')) { + $pdo->exec('ALTER TABLE attendees ADD COLUMN consented TINYINT(1) NOT NULL DEFAULT 0 AFTER password'); + } + + if (!schema_column_exists($pdo, 'attendees', 'deleted_at')) { + $pdo->exec('ALTER TABLE attendees ADD COLUMN deleted_at TIMESTAMP NULL DEFAULT NULL AFTER consented'); + } + + if (!schema_column_exists($pdo, 'attendees', 'created_at')) { + $pdo->exec('ALTER TABLE attendees ADD COLUMN created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP AFTER company'); + } + + if (schema_column_exists($pdo, 'attendees', 'registered_at')) { + $pdo->exec('UPDATE attendees SET created_at = COALESCE(created_at, registered_at)'); + } + + if (schema_column_exists($pdo, 'attendees', 'name')) { + $pdo->exec("UPDATE attendees SET first_name = TRIM(SUBSTRING_INDEX(name, ' ', 1)) WHERE (first_name = '' OR first_name IS NULL) AND name IS NOT NULL AND name <> ''"); + $pdo->exec("UPDATE attendees SET last_name = TRIM(SUBSTRING(name, CHAR_LENGTH(SUBSTRING_INDEX(name, ' ', 1)) + 1)) WHERE (last_name = '' OR last_name IS NULL) AND name IS NOT NULL AND name LIKE '% %'"); + } + } + + $desiredTitle = 'Building Scalable Apps with AppWizzy'; + $desiredDescription = 'The fastest way to go from an idea to a working app you own, running on your server, with your database, using real frameworks.'; + $desiredPresenter = 'AppWizzy Team'; + $desiredScheduledAt = '2026-03-25 18:00:00'; + + $stmt = $pdo->prepare('SELECT COUNT(*) FROM webinars WHERE id = 1'); + $stmt->execute(); + $hasPrimaryWebinar = (int) $stmt->fetchColumn() > 0; + + if ($hasPrimaryWebinar) { + $update = $pdo->prepare('UPDATE webinars SET title = ?, description = ?, presenter = ?, scheduled_at = ? WHERE id = 1'); + $update->execute([$desiredTitle, $desiredDescription, $desiredPresenter, $desiredScheduledAt]); + } else { + $insert = $pdo->prepare('INSERT INTO webinars (id, title, description, presenter, scheduled_at) VALUES (1, ?, ?, ?, ?)'); + $insert->execute([$desiredTitle, $desiredDescription, $desiredPresenter, $desiredScheduledAt]); + } +} diff --git a/db/migrations/007_add_consented_column.sql b/db/migrations/007_add_consented_column.sql index f6aefbb..184f6c3 100644 --- a/db/migrations/007_add_consented_column.sql +++ b/db/migrations/007_add_consented_column.sql @@ -1 +1 @@ -ALTER TABLE `attendees` ADD `consented` TINYINT(1) NOT NULL DEFAULT 0; \ No newline at end of file +ALTER TABLE attendees ADD COLUMN IF NOT EXISTS consented TINYINT(1) NOT NULL DEFAULT 0; diff --git a/db/migrations/011_align_attendees_schema_and_current_webinar.sql b/db/migrations/011_align_attendees_schema_and_current_webinar.sql new file mode 100644 index 0000000..6e21227 --- /dev/null +++ b/db/migrations/011_align_attendees_schema_and_current_webinar.sql @@ -0,0 +1,30 @@ +ALTER TABLE attendees MODIFY COLUMN name VARCHAR(255) NULL; +ALTER TABLE attendees ADD COLUMN IF NOT EXISTS first_name VARCHAR(255) NOT NULL DEFAULT '' AFTER webinar_id; +ALTER TABLE attendees ADD COLUMN IF NOT EXISTS last_name VARCHAR(255) NOT NULL DEFAULT '' AFTER first_name; +ALTER TABLE attendees ADD COLUMN IF NOT EXISTS created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP AFTER company; +ALTER TABLE attendees ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP NULL DEFAULT NULL AFTER consented; +ALTER TABLE attendees ADD COLUMN IF NOT EXISTS how_did_you_hear VARCHAR(255) DEFAULT NULL AFTER timezone; +UPDATE attendees SET created_at = COALESCE(created_at, registered_at); +UPDATE attendees +SET first_name = TRIM(SUBSTRING_INDEX(name, ' ', 1)) +WHERE (first_name = '' OR first_name IS NULL) + AND name IS NOT NULL + AND name <> ''; +UPDATE attendees +SET last_name = TRIM(SUBSTRING(name, CHAR_LENGTH(SUBSTRING_INDEX(name, ' ', 1)) + 1)) +WHERE (last_name = '' OR last_name IS NULL) + AND name IS NOT NULL + AND name LIKE '% %'; +INSERT INTO webinars (id, title, description, presenter, scheduled_at) +SELECT 1, + 'Building Scalable Apps with AppWizzy', + 'The fastest way to go from an idea to a working app you own, running on your server, with your database, using real frameworks.', + 'AppWizzy Team', + '2026-03-25 18:00:00' +WHERE NOT EXISTS (SELECT 1 FROM webinars WHERE id = 1); +UPDATE webinars +SET title = 'Building Scalable Apps with AppWizzy', + description = 'The fastest way to go from an idea to a working app you own, running on your server, with your database, using real frameworks.', + presenter = 'AppWizzy Team', + scheduled_at = '2026-03-25 18:00:00' +WHERE id = 1; diff --git a/delete_attendee.php b/delete_attendee.php index dbce2ef..25495cd 100644 --- a/delete_attendee.php +++ b/delete_attendee.php @@ -7,22 +7,19 @@ if (!isset($_SESSION['user']) || $_SESSION['user'] !== 'admin') { exit; } -if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['id'])) { - $id = $_POST['id']; +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['id']) && is_numeric($_POST['id'])) { + $id = (int) $_POST['id']; $pdo = db(); - $stmt = $pdo->prepare("DELETE FROM attendees WHERE id = ?"); - - if ($stmt->execute([$id])) { - if ($stmt->rowCount() > 0) { - $_SESSION['message'] = "Attendee with ID $id has been deleted successfully."; - } else { - $_SESSION['message'] = "Error: No attendee found with ID $id. Nothing was deleted."; - } + $stmt = $pdo->prepare('UPDATE attendees SET deleted_at = NOW() WHERE id = ? AND deleted_at IS NULL'); + $stmt->execute([$id]); + + if ($stmt->rowCount() > 0) { + $_SESSION['message'] = "Attendee #{$id} has been archived successfully."; } else { - $_SESSION['message'] = "Error: Could not execute the delete statement."; + $_SESSION['message'] = "No active attendee found for ID #{$id}."; } } else { - $_SESSION['message'] = "Error: Invalid request."; + $_SESSION['message'] = 'Error: Invalid archive request.'; } header('Location: admin.php'); diff --git a/export_csv.php b/export_csv.php index 3c0d61e..e090c5a 100644 --- a/export_csv.php +++ b/export_csv.php @@ -2,36 +2,78 @@ session_start(); require_once 'db/config.php'; -// Check if user is admin if (!isset($_SESSION['user']) || $_SESSION['user'] !== 'admin') { http_response_code(403); - echo "Forbidden"; + echo 'Forbidden'; exit; } $pdo = db(); -$stmt = $pdo->query("SELECT first_name, last_name, email, how_did_you_hear, company FROM attendees ORDER BY created_at DESC"); +$selected_webinar_id = isset($_GET['webinar_id']) && is_numeric($_GET['webinar_id']) ? max(0, (int) $_GET['webinar_id']) : 0; + +$where_sql = 'WHERE a.deleted_at IS NULL'; +$params = []; +if ($selected_webinar_id > 0) { + $where_sql .= ' AND a.webinar_id = :webinar_id'; + $params[':webinar_id'] = $selected_webinar_id; +} + +$sql = "SELECT + a.id, + a.webinar_id, + w.title AS webinar_title, + a.first_name, + a.last_name, + a.email, + a.company, + a.how_did_you_hear, + a.timezone, + a.consented, + a.created_at + FROM attendees a + LEFT JOIN webinars w ON w.id = a.webinar_id + {$where_sql} + ORDER BY a.created_at DESC, a.id DESC"; +$stmt = $pdo->prepare($sql); +foreach ($params as $name => $value) { + $stmt->bindValue($name, $value, PDO::PARAM_INT); +} +$stmt->execute(); $attendees = $stmt->fetchAll(PDO::FETCH_ASSOC); header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename=attendees.csv'); $output = fopen('php://output', 'w'); - -// Add BOM to fix UTF-8 in Excel fputs($output, "\xEF\xBB\xBF"); -// Add header row -fputcsv($output, ['First Name', 'Last Name', 'Email', 'Source', 'Company']); +fputcsv($output, [ + 'ID', + 'Webinar ID', + 'Webinar Title', + 'First Name', + 'Last Name', + 'Email', + 'Company', + 'Source', + 'Timezone', + 'Consented', + 'Registered At', +]); -// Add data rows foreach ($attendees as $attendee) { fputcsv($output, [ + $attendee['id'], + $attendee['webinar_id'], + $attendee['webinar_title'] ?? '', $attendee['first_name'], $attendee['last_name'], $attendee['email'], + $attendee['company'] ?? '', $attendee['how_did_you_hear'] ?? '', - $attendee['company'] ?? '' + $attendee['timezone'] ?? '', + !empty($attendee['consented']) ? 'Yes' : 'No', + $attendee['created_at'] ?? '', ]); } diff --git a/index.php b/index.php index c38923f..6361b6e 100644 --- a/index.php +++ b/index.php @@ -325,6 +325,7 @@
+
@@ -353,6 +354,7 @@ const messageDiv = document.getElementById('form-message'); const rightColumn = document.getElementById('right-column-content'); const emailInput = form.querySelector('input[name="email"]'); + const timezoneField = form.querySelector('#timezone-field'); const emailValue = (emailInput.value || '').trim().toLowerCase(); const blockedDomains = new Set([ '10minutemail.com', @@ -376,6 +378,14 @@ messageDiv.textContent = ''; // Clear previous messages + if (timezoneField) { + try { + timezoneField.value = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; + } catch (error) { + timezoneField.value = ''; + } + } + if (emailValue && emailValue.includes('@')) { const emailDomain = emailValue.split('@').pop(); if (blockedDomains.has(emailDomain)) { diff --git a/register.php b/register.php index fe380f9..2e9eac2 100644 Binary files a/register.php and b/register.php differ