From 5cae9128b61a88e72b2e3c3f9daec17af8b0b49b Mon Sep 17 00:00:00 2001
From: bengizmo
Date: Mon, 7 Apr 2025 06:33:21 -0300
Subject: [PATCH] Initial commit for Cloudways deployment source (plugin only)
---
.../assets/css/community-login.css | 96 ++
.../assets/css/hvac-dashboard.css | 196 +++
.../assets/css/hvac-event-summary.css | 71 ++
.../assets/css/hvac-registration.css | 106 ++
.../assets/js/hvac-registration.js | 78 ++
.../hvac-community-events.php | 204 ++++
.../includes/class-hvac-community-events.php | 152 +++
.../includes/class-hvac-dashboard-data.php | 312 +++++
.../includes/class-hvac-registration.php | 1066 +++++++++++++++++
.../includes/class-hvac-roles.php | 93 ++
.../includes/class-hvac-settings.php | 71 ++
.../community/class-event-handler.php | 62 +
.../community/class-event-summary-data.php | 227 ++++
.../includes/community/class-hvac-profile.php | 979 +++++++++++++++
.../community/class-login-handler.php | 174 +++
.../templates/community/login-form.php | 67 ++
.../templates/single-hvac-event-summary.php | 222 ++++
.../templates/template-hvac-dashboard.php | 220 ++++
18 files changed, 4396 insertions(+)
create mode 100644 hvac-community-events/assets/css/community-login.css
create mode 100644 hvac-community-events/assets/css/hvac-dashboard.css
create mode 100644 hvac-community-events/assets/css/hvac-event-summary.css
create mode 100644 hvac-community-events/assets/css/hvac-registration.css
create mode 100644 hvac-community-events/assets/js/hvac-registration.js
create mode 100644 hvac-community-events/hvac-community-events.php
create mode 100644 hvac-community-events/includes/class-hvac-community-events.php
create mode 100644 hvac-community-events/includes/class-hvac-dashboard-data.php
create mode 100644 hvac-community-events/includes/class-hvac-registration.php
create mode 100644 hvac-community-events/includes/class-hvac-roles.php
create mode 100644 hvac-community-events/includes/class-hvac-settings.php
create mode 100644 hvac-community-events/includes/community/class-event-handler.php
create mode 100644 hvac-community-events/includes/community/class-event-summary-data.php
create mode 100644 hvac-community-events/includes/community/class-hvac-profile.php
create mode 100644 hvac-community-events/includes/community/class-login-handler.php
create mode 100644 hvac-community-events/templates/community/login-form.php
create mode 100644 hvac-community-events/templates/single-hvac-event-summary.php
create mode 100644 hvac-community-events/templates/template-hvac-dashboard.php
diff --git a/hvac-community-events/assets/css/community-login.css b/hvac-community-events/assets/css/community-login.css
new file mode 100644
index 00000000..391c8bc5
--- /dev/null
+++ b/hvac-community-events/assets/css/community-login.css
@@ -0,0 +1,96 @@
+/**
+ * HVAC Community Events: Community Login Styles
+ *
+ * Styles for the custom login form page.
+ *
+ * @version 1.0.0
+ */
+
+.hvac-community-login-wrapper {
+ /* Add styles to center the card vertically/horizontally if needed */
+ padding: 40px 0; /* Example padding */
+}
+
+.hvac-login-form-card {
+ max-width: 400px; /* Adjust as needed based on design */
+ margin: 0 auto;
+ padding: 30px;
+ background-color: #ffffff; /* White card background */
+ border: 1px solid #e0e0e0; /* Light border */
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1); /* Subtle shadow */
+ border-radius: 4px; /* Rounded corners */
+}
+
+/* Style the form generated by wp_login_form */
+#hvac_community_loginform p {
+ margin-bottom: 15px;
+}
+
+#hvac_community_loginform label {
+ display: block;
+ margin-bottom: 5px;
+ font-weight: bold;
+}
+
+#hvac_community_loginform input[type="text"],
+#hvac_community_loginform input[type="password"] {
+ width: 100%;
+ padding: 10px;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ box-sizing: border-box; /* Include padding and border in the element's total width and height */
+}
+
+#hvac_community_loginform .login-remember label {
+ font-weight: normal;
+ display: inline-block; /* Align checkbox and label */
+}
+#hvac_community_loginform .login-remember input[type="checkbox"] {
+ margin-right: 5px;
+ vertical-align: middle;
+}
+
+
+#hvac_community_loginform .login-submit #wp-submit {
+ /* Use Astra button styles if possible, or define custom */
+ /* Example using Astra's class structure (might need adjustment) */
+ /* @extend .ast-button; */
+ display: inline-block;
+ padding: 10px 20px;
+ background-color: #0073aa; /* Example blue */
+ color: #ffffff;
+ border: none;
+ border-radius: 3px;
+ cursor: pointer;
+ text-decoration: none;
+ font-size: 1em;
+ width: 100%; /* Make button full width */
+ text-align: center;
+}
+
+#hvac_community_loginform .login-submit #wp-submit:hover {
+ background-color: #005a87; /* Darker blue on hover */
+}
+
+.hvac-login-links {
+ margin-top: 20px;
+ text-align: center;
+ font-size: 0.9em;
+}
+
+.hvac-login-links a {
+ color: #0073aa; /* Link color */
+ text-decoration: none;
+}
+
+.hvac-login-links a:hover {
+ text-decoration: underline;
+}
+
+/* Add responsive adjustments if needed */
+@media (max-width: 544px) { /* Example using Astra mobile breakpoint */
+ .hvac-login-form-card {
+ max-width: 90%;
+ padding: 20px;
+ }
+}
\ No newline at end of file
diff --git a/hvac-community-events/assets/css/hvac-dashboard.css b/hvac-community-events/assets/css/hvac-dashboard.css
new file mode 100644
index 00000000..c5265c31
--- /dev/null
+++ b/hvac-community-events/assets/css/hvac-dashboard.css
@@ -0,0 +1,196 @@
+/*
+ * HVAC Trainer Dashboard Styles
+ *
+ * Styles specific to the template-hvac-dashboard.php template.
+ */
+/* General Page Styles */
+body.page-template-template-hvac-dashboard {
+ background-color: #f0f4f8; /* Light blue background - Adjust color as needed */
+}
+
+.section-title {
+ font-size: 1.5em; /* Example size */
+ margin-bottom: 1em;
+ color: #333; /* Adjust color as needed */
+}
+
+
+/* Header */
+.hvac-dashboard-header {
+ margin-bottom: 2em;
+ padding-bottom: 1em;
+ border-bottom: 1px solid #eee; /* Consider using theme variable for border color */
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap; /* Allow wrapping on smaller screens */
+}
+
+.hvac-dashboard-nav a {
+ margin-left: 0.5em; /* Add some space between nav buttons */
+}
+
+/* Stats Section */
+.hvac-dashboard-stats {
+ margin-bottom: 2em;
+}
+
+/* Use flexbox for 5 columns */
+.hvac-stats-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 20px; /* Adjust gap as needed */
+ margin-left: -10px; /* Counteract column padding */
+ margin-right: -10px; /* Counteract column padding */
+}
+
+.hvac-stats-grid > .ast-col {
+ flex: 1 1 calc(20% - 20px); /* 5 columns minus gap */
+ padding: 10px;
+ display: flex;
+ flex-direction: column;
+ min-width: 150px; /* Prevent cards from becoming too small */
+}
+
+/* Responsive adjustments for stats grid */
+@media (max-width: 921px) { /* Astra Tablet Breakpoint */
+ .hvac-stats-grid > .ast-col {
+ flex: 1 1 calc(33.333% - 20px); /* 3 columns */
+ }
+}
+@media (max-width: 544px) { /* Astra Mobile Breakpoint */
+ .hvac-stats-grid > .ast-col {
+ flex: 1 1 calc(50% - 20px); /* 2 columns */
+ }
+}
+
+.hvac-stat-card {
+ border: 1px solid #e0e0e0; /* Lighter border */
+ padding: 20px;
+ background: #fff;
+ text-align: center;
+ width: 100%;
+ flex-grow: 1;
+ border-radius: 4px; /* Slight rounding */
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05); /* Subtle shadow */
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+/* TODO: Add styles for icons using ::before or background images */
+/* Example:
+.hvac-stat-card::before {
+ content: '';
+ display: block;
+ width: 30px;
+ height: 30px;
+ background-color: #ccc; // Placeholder
+ margin-bottom: 10px;
+}
+*/
+
+.hvac-stat-card .stat-value {
+ font-size: 2.2em;
+ font-weight: 600; /* Slightly bolder */
+ color: #0073aa; /* Example blue color - use theme variable if possible */
+ line-height: 1.1;
+ margin-bottom: 0.1em;
+ display: block;
+}
+
+.hvac-stat-card .stat-label {
+ font-size: 0.9em;
+ color: #555; /* Darker grey */
+ display: block;
+}
+
+/* Remove old h3/p/small styles if no longer used */
+.hvac-stat-card h3,
+.hvac-stat-card p,
+.hvac-stat-card small {
+ display: none; /* Hide old elements if PHP was updated */
+}
+
+
+/* Event Tabs */
+.hvac-event-tabs {
+ margin-bottom: 1.5em;
+ border-bottom: 1px solid #ccc; /* Separator line */
+}
+
+.hvac-tabs-nav {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+}
+
+.hvac-tabs-nav li {
+ margin: 0;
+ padding: 0;
+}
+
+.hvac-tab-link {
+ display: block;
+ padding: 0.8em 1.2em;
+ text-decoration: none;
+ color: #555;
+ border: 1px solid transparent;
+ border-bottom: none;
+ margin-bottom: -1px; /* Overlap border-bottom */
+ position: relative;
+ background-color: transparent;
+ transition: all 0.2s ease-in-out;
+}
+
+.hvac-tab-link:hover {
+ color: #0073aa; /* Theme primary color */
+ background-color: #f9f9f9;
+}
+
+.hvac-tab-link.active {
+ color: #333;
+ font-weight: 600;
+ background-color: #fff; /* Match page background if needed */
+ border-color: #ccc #ccc #fff; /* Border to create tab effect */
+ border-top-left-radius: 3px;
+ border-top-right-radius: 3px;
+}
+
+/* Hide old button filters if PHP was updated */
+.hvac-event-filters {
+ display: none;
+}
+
+/* Events Table */
+.hvac-events-table-wrapper {
+ overflow-x: auto; /* Add horizontal scroll for smaller screens */
+}
+
+/* Ensure table uses standard WP/Theme styling and add custom class */
+.hvac-events-table {
+ margin-top: 1em;
+ background-color: #fff; /* White background for the table card */
+ border: 1px solid #e0e0e0;
+ border-radius: 4px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+ padding: 15px; /* Add padding inside the card */
+}
+
+/* Ensure striped rows are visible */
+.hvac-events-table.striped tbody tr:nth-child(odd) {
+ background-color: #f9f9f9; /* Adjust stripe color if needed */
+}
+
+.hvac-events-table .column-actions .ast-button {
+ margin-right: 0.5em;
+ padding: 0.3em 0.8em; /* Make buttons smaller */
+ font-size: 0.85em;
+ /* TODO: Add icon styles using ::before */
+}
+
+.hvac-events-table .column-actions .ast-button:last-child {
+ margin-right: 0;
+}
\ No newline at end of file
diff --git a/hvac-community-events/assets/css/hvac-event-summary.css b/hvac-community-events/assets/css/hvac-event-summary.css
new file mode 100644
index 00000000..ac99f86e
--- /dev/null
+++ b/hvac-community-events/assets/css/hvac-event-summary.css
@@ -0,0 +1,71 @@
+/**
+ * Styles for the HVAC Community Events Single Event Summary Template
+ */
+
+.hvac-event-summary-details,
+.hvac-event-summary-transactions {
+ margin-bottom: 2em; /* Add spacing between sections */
+ padding: 1.5em;
+ border: 1px solid #e2e2e2; /* Basic border like theme cards */
+ border-radius: 4px; /* Slight rounding */
+ background-color: #fff; /* White background */
+}
+
+.hvac-event-summary-details h2,
+.hvac-event-summary-transactions h2 {
+ margin-top: 0;
+ margin-bottom: 1em;
+ font-size: 1.5em; /* Adjust as needed */
+ border-bottom: 1px solid #eee;
+ padding-bottom: 0.5em;
+}
+
+.hvac-event-summary-details h3 {
+ margin-top: 1.5em;
+ margin-bottom: 0.5em;
+ font-size: 1.2em;
+}
+
+.hvac-event-summary-details p,
+.hvac-event-summary-transactions p {
+ margin-bottom: 0.8em;
+}
+
+.hvac-event-summary-details .event-description {
+ margin-top: 1em;
+ padding-top: 1em;
+ border-top: 1px dashed #eee;
+}
+
+/* Basic Table Styling - Inherit Astra's base styles where possible */
+.hvac-transactions-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 1em;
+}
+
+.hvac-transactions-table th,
+.hvac-transactions-table td {
+ text-align: left;
+ padding: 0.8em 1em;
+ border-bottom: 1px solid #eee;
+}
+
+.hvac-transactions-table th {
+ background-color: #f8f8f8; /* Light background for header */
+ font-weight: bold;
+}
+
+.hvac-transactions-table tbody tr:nth-child(odd) {
+ background-color: #fdfdfd; /* Subtle striping */
+}
+
+.hvac-transactions-table tbody tr:hover {
+ background-color: #f1f1f1; /* Hover effect */
+}
+
+/* Ensure edit button has some margin */
+.entry-header .button.astra-button {
+ margin-left: 1em;
+ vertical-align: middle; /* Align with title */
+}
\ No newline at end of file
diff --git a/hvac-community-events/assets/css/hvac-registration.css b/hvac-community-events/assets/css/hvac-registration.css
new file mode 100644
index 00000000..5288c2fb
--- /dev/null
+++ b/hvac-community-events/assets/css/hvac-registration.css
@@ -0,0 +1,106 @@
+/* HVAC Trainer Registration Form Styles */
+.hvac-registration-form {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 2rem;
+ background: #fff;
+ border-radius: 4px;
+ box-shadow: 0 0 10px rgba(0,0,0,0.05);
+}
+
+.hvac-registration-form h2 {
+ color: #1a1a1a;
+ margin-bottom: 1.5rem;
+ text-align: center;
+}
+
+.hvac-registration-form .form-section {
+ margin-bottom: 2rem;
+ padding-bottom: 1.5rem;
+ border-bottom: 1px solid #eee;
+}
+
+.hvac-registration-form .form-section h3 {
+ color: #1a1a1a;
+ margin-bottom: 1rem;
+}
+
+.hvac-registration-form .form-row {
+ margin-bottom: 1rem;
+}
+
+.hvac-registration-form label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-weight: 600;
+ color: #1a1a1a;
+}
+
+.hvac-registration-form input[type="text"],
+.hvac-registration-form input[type="email"],
+.hvac-registration-form input[type="password"],
+.hvac-registration-form input[type="url"],
+.hvac-registration-form textarea,
+.hvac-registration-form select {
+ width: 100%;
+ padding: 0.75rem;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 1rem;
+}
+
+.hvac-registration-form .form-submit {
+ margin-top: 2rem;
+ text-align: center;
+}
+
+.hvac-registration-form input[type="submit"] {
+ background-color: #0274be;
+ color: white;
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 1rem;
+ transition: background-color 0.3s;
+}
+
+.hvac-registration-form input[type="submit"]:hover {
+ background-color: #0261a0;
+}
+
+/* Checkbox/Radio Group Styles */
+.hvac-registration-form .checkbox-group {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ margin-top: 0.5rem;
+}
+
+.hvac-registration-form .checkbox-group label {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: normal;
+ cursor: pointer;
+}
+
+.hvac-registration-form .checkbox-group input[type="checkbox"],
+.hvac-registration-form .checkbox-group input[type="radio"] {
+ width: auto;
+ margin: 0;
+}
+
+/* Error message styling */
+.hvac-registration-form .hvac-errors {
+ background-color: #fff0f0;
+ border: 1px solid #ffcccc;
+ border-radius: 4px;
+ padding: 1rem;
+ margin-bottom: 1.5rem;
+}
+
+.hvac-registration-form .hvac-errors .error {
+ color: #d63638;
+ margin: 0.25rem 0;
+}
\ No newline at end of file
diff --git a/hvac-community-events/assets/js/hvac-registration.js b/hvac-community-events/assets/js/hvac-registration.js
new file mode 100644
index 00000000..b01c90a1
--- /dev/null
+++ b/hvac-community-events/assets/js/hvac-registration.js
@@ -0,0 +1,78 @@
+jQuery(document).ready(function($) {
+ const $countrySelect = $('#user_country');
+ const $stateSelect = $('#user_state');
+ const $stateOtherInput = $('#user_state_other');
+
+ // Function to populate states/provinces
+ function loadStates(country) {
+ console.log(`[DEBUG] loadStates called for country: ${country}`); // More specific log
+ $stateSelect.find('option').not('[value=""],[value="Other"]').remove(); // Clear existing options except defaults
+ console.log(`[DEBUG] Cleared existing state options.`);
+
+ let options = {};
+ let dataSource = 'none';
+ // <<< CORRECTED: Use hvac_reg_vars instead of hvacRegistrationData
+ if (country === 'United States' && typeof hvac_reg_vars !== 'undefined' && hvac_reg_vars.us_states) {
+ options = hvac_reg_vars.us_states;
+ dataSource = 'us_states';
+ } else if (country === 'Canada' && typeof hvac_reg_vars !== 'undefined' && hvac_reg_vars.ca_provinces) {
+ options = hvac_reg_vars.ca_provinces;
+ dataSource = 'ca_provinces';
+ } else {
+ // If country is not US/CA or data is missing, ensure 'Other' is selected and input shown
+ $stateSelect.val('Other').trigger('change'); // Trigger change to show 'Other' input if needed
+ return;
+ }
+ console.log(`[DEBUG] Data source: ${dataSource}, Options found: ${Object.keys(options).length}`);
+
+ // Append new options
+ let optionsAppended = 0;
+ $.each(options, function(value, label) {
+ optionsAppended++;
+ // Append before the 'Other' option if it exists, otherwise just append
+ const $otherOption = $stateSelect.find('option[value="Other"]');
+ const $newOption = $('').val(value).text(label);
+ if ($otherOption.length > 0) {
+ $newOption.insertBefore($otherOption);
+ } else {
+ $stateSelect.append($newOption);
+ }
+ });
+ console.log(`[DEBUG] Appended ${optionsAppended} state/province options.`);
+
+ // Ensure the 'Other' input is hidden initially when states/provinces are loaded
+ $stateOtherInput.hide().val('');
+ // Reset state selection to default prompt
+ $stateSelect.val('');
+ }
+
+ // Handle state/province field visibility based on 'Other' selection
+ $stateSelect.change(function() {
+ if ($(this).val() === 'Other') {
+ $stateOtherInput.show().prop('required', true); // Make required if Other is selected
+ } else {
+ $stateOtherInput.hide().val('').prop('required', false); // Hide and make not required
+ }
+ }).trigger('change'); // Trigger on load to set initial visibility
+
+ // Handle country change to show/hide/populate state field
+ $countrySelect.change(function() {
+ const country = $(this).val();
+ console.log(`[DEBUG] Country changed to: ${country}`); // Log country change
+
+ if (country === 'United States' || country === 'Canada') {
+ loadStates(country);
+ $stateSelect.show().prop('required', true); // Show and require state select
+ $stateOtherInput.prop('required', false); // Ensure 'Other' input is not required initially
+ } else if (country) {
+ // For other countries, hide state select, select 'Other', show/require 'Other' input
+ $stateSelect.hide().val('Other').prop('required', false); // Hide and make not required
+ $stateOtherInput.show().prop('required', true); // Show and require 'Other' input
+ } else {
+ // No country selected
+ $stateSelect.hide().val('').prop('required', false); // Hide and make not required
+ $stateOtherInput.hide().val('').prop('required', false); // Hide and make not required
+ }
+ }).trigger('change'); // Trigger on load to set initial state based on pre-selected country (if any)
+
+});
\ No newline at end of file
diff --git a/hvac-community-events/hvac-community-events.php b/hvac-community-events/hvac-community-events.php
new file mode 100644
index 00000000..4bbb0b62
--- /dev/null
+++ b/hvac-community-events/hvac-community-events.php
@@ -0,0 +1,204 @@
+ [
+ 'title' => 'Community Login',
+ 'content' => '[hvac_community_login]',
+ ],
+ 'trainer-registration' => [
+ 'title' => 'Trainer Registration',
+ 'content' => '[hvac_trainer_registration]',
+ ],
+ 'hvac-dashboard' => [
+ 'title' => 'Trainer Dashboard',
+ 'content' => '', // Content handled by template or redirect
+ ],
+ 'manage-event' => [ // New page for TEC CE submission form shortcode
+ 'title' => 'Manage Event',
+ 'content' => '[tribe_community_events view="submission_form"]',
+ ],
+ 'my-events' => [ // New page for TEC CE event list shortcode
+ 'title' => 'My Events',
+ 'content' => '[tribe_community_events view="my_events"]',
+ ],
+ 'trainer-profile' => [ // Add Trainer Profile page
+ 'title' => 'Trainer Profile',
+ 'content' => '[hvac_trainer_profile]',
+ ],
+ // REMOVED: 'submit-event' page creation. Will link to default TEC CE page.
+ // Add future required pages here
+ ];
+
+ $created_pages_option = 'hvac_community_pages';
+ $created_pages = get_option($created_pages_option, []);
+
+ foreach ($required_pages as $slug => $page_data) {
+ // Check if page already exists (by slug)
+ $existing_page = get_page_by_path($slug, OBJECT, 'page');
+
+ if (!$existing_page) {
+ error_log("HVAC CE: Page with slug '{$slug}' not found. Attempting to create."); // Add logging: page missing
+ // Page does not exist, create it
+ $post_data = [
+ 'post_title' => $page_data['title'],
+ 'post_name' => $slug,
+ 'post_content' => $page_data['content'],
+ 'post_status' => 'publish',
+ 'post_type' => 'page',
+ 'comment_status' => 'closed',
+ 'ping_status' => 'closed',
+ ];
+
+ $page_id = wp_insert_post($post_data);
+
+ // Log the result of wp_insert_post
+ if (is_wp_error($page_id)) {
+ error_log("HVAC CE: Error creating page '{$slug}': " . $page_id->get_error_message());
+ } else {
+ error_log("HVAC CE: Successfully created page '{$slug}' with ID: {$page_id}.");
+ }
+
+ // Store the created page ID - Rewritten to avoid tool issue with &&
+ if ($page_id) { // Check if page_id is truthy (non-zero, non-null, etc.)
+ if (!is_wp_error($page_id)) { // Then check if it's not a WP_Error object
+ // Use a key based on the slug or feature name for clarity
+ $feature_key = str_replace('-', '_', $slug);
+ $created_pages[$feature_key] = $page_id;
+ }
+ }
+ } else {
+ // Ensure existing pages are also recorded in the option if not already
+ $feature_key = str_replace('-', '_', $slug);
+ if (!isset($created_pages[$feature_key])) {
+ $created_pages[$feature_key] = $existing_page->ID;
+ }
+ }
+ }
+
+ // Update the option with any newly created page IDs (and existing ones)
+ update_option($created_pages_option, $created_pages);
+
+ // Create the custom role (Moved inside the activation function)
+ $roles_manager = new HVAC_Roles();
+ $roles_manager->create_trainer_role();
+ error_log('HVAC CE: Attempted to create hvac_trainer role.'); // Add logging: role creation attempt
+
+} // <<-- Brace moved here
+register_activation_hook(__FILE__, 'hvac_ce_create_required_pages');
+
+
+/**
+ * Remove custom roles upon plugin deactivation.
+ */
+function hvac_ce_remove_roles() {
+ require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-roles.php';
+ $roles_manager = new HVAC_Roles();
+ $roles_manager->remove_trainer_role();
+ error_log('HVAC CE: Deactivation hook fired, attempted to remove hvac_trainer role.');
+}
+register_deactivation_hook(__FILE__, 'hvac_ce_remove_roles');
+
+
+
+/**
+ * Enqueue styles specifically for the HVAC Dashboard page.
+ */
+function hvac_ce_enqueue_dashboard_styles() {
+ // Check if we are on the specific dashboard page
+ // Assumes the page slug is 'hvac-dashboard' as created in the activation hook
+ if ( is_page( 'hvac-dashboard' ) ) {
+ wp_enqueue_style(
+ 'hvac-dashboard-style',
+ HVAC_CE_PLUGIN_URL . 'assets/css/hvac-dashboard.css',
+ [], // No dependencies for now
+ HVAC_CE_VERSION
+ );
+ }
+}
+add_action( 'wp_enqueue_scripts', 'hvac_ce_enqueue_dashboard_styles' );
+
+
+/**
+ * Enqueue styles specifically for the HVAC Event Summary page.
+ */
+function hvac_ce_enqueue_event_summary_styles() {
+ // Check if we are on a single event page
+ if ( is_singular( Tribe__Events__Main::POSTTYPE ) ) {
+ wp_enqueue_style(
+ 'hvac-event-summary-style',
+ HVAC_CE_PLUGIN_URL . 'assets/css/hvac-event-summary.css',
+ [], // No dependencies for now
+ HVAC_CE_VERSION
+ );
+ }
+}
+add_action( 'wp_enqueue_scripts', 'hvac_ce_enqueue_event_summary_styles' );
+
+
+
+// Include the main plugin class
+require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-community-events.php'; // Main plugin class
+require_once HVAC_CE_PLUGIN_DIR . 'includes/community/class-hvac-profile.php'; // Include the new Profile class
+// Initialize the plugin via plugins_loaded hook
+function hvac_community_events_init() {
+ // Use Singleton pattern for both classes
+ HVAC_Community_Events::instance();
+ HVAC_Profile::instance(); // Restore instantiation here
+}
+add_action('plugins_loaded', 'hvac_community_events_init');
+
+// REMOVED: Instantiate directly AFTER function definition to ensure hooks are added for E2E tests
+// HVAC_Profile::instance();
+
+
+/**
+ * Include custom template for single event summary page.
+ *
+ * @param string $template The path of the template to include.
+ * @return string The path of the template file.
+ */
+function hvac_ce_include_event_summary_template( $template ) {
+ // Check if it's a single event post type view
+ if ( is_singular( Tribe__Events__Main::POSTTYPE ) ) {
+ // Check if the custom template exists in the plugin's template directory
+ $custom_template = HVAC_CE_PLUGIN_DIR . 'templates/single-hvac-event-summary.php';
+ if ( file_exists( $custom_template ) ) {
+ // Return the path to the custom template
+ return $custom_template;
+ }
+ }
+ // Return the original template if not a single event or custom template doesn't exist
+ return $template;
+}
+add_filter( 'template_include', 'hvac_ce_include_event_summary_template', 99 );
diff --git a/hvac-community-events/includes/class-hvac-community-events.php b/hvac-community-events/includes/class-hvac-community-events.php
new file mode 100644
index 00000000..764a21d1
--- /dev/null
+++ b/hvac-community-events/includes/class-hvac-community-events.php
@@ -0,0 +1,152 @@
+define_constants();
+ $this->includes();
+ $this->init_hooks();
+ }
+
+ /**
+ * Define constants
+ */
+ private function define_constants() {
+ // Additional constants can be defined here
+ }
+
+ /**
+ * Include required files
+ */
+ private function includes() {
+ require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-roles.php';
+ require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-registration.php';
+ require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-settings.php';
+ require_once HVAC_CE_PLUGIN_DIR . 'includes/community/class-login-handler.php'; // Add Login Handler
+ require_once HVAC_CE_PLUGIN_DIR . 'includes/community/class-event-handler.php'; // Add Event Handler
+ // Include dashboard data class if it's not autoloaded
+ if ( ! class_exists('HVAC_Dashboard_Data') ) {
+ require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-dashboard-data.php';
+ }
+ }
+
+ /**
+ * Initialize hooks
+ */
+ private function init_hooks() {
+ // Register activation/deactivation hooks
+ // Note: These hooks are typically registered outside the class instance context
+ // register_activation_hook(__FILE__, array($this, 'activate')); // This won't work correctly here
+ // register_deactivation_hook(__FILE__, array($this, 'deactivate')); // This won't work correctly here
+
+ // Initialize other hooks
+ add_action('init', array($this, 'init'));
+
+ // Template loading for custom pages
+ add_filter('template_include', array($this, 'load_custom_templates'));
+ } // End init_hooks
+
+ /**
+ * Plugin activation (Should be called statically or from the main plugin file context)
+ */
+ public static function activate() {
+ // Activation code here (e.g., page creation, role creation)
+ // Note: This method might need to be moved or called differently
+ }
+
+ /**
+ * Plugin deactivation (Should be called statically or from the main plugin file context)
+ */
+ public static function deactivate() {
+ // Remove the hvac_trainer role
+ require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-roles.php'; // Ensure class is available
+ $roles = new HVAC_Roles();
+ $roles->remove_trainer_role();
+
+ // Additional deactivation tasks
+ // ...
+ }
+
+ /**
+ * Initialize plugin actions attached to 'init' hook
+ */
+ public function init() {
+ static $initialized = false; // Flag to run only once
+ if ($initialized) {
+ return;
+ }
+
+ // Initialize handlers
+ error_log('[HVAC DEBUG] HVAC_Community_Events::init() - Before new Login_Handler()'); // Updated Log
+ new \HVAC_Community_Events\Community\Login_Handler();
+ error_log('[HVAC DEBUG] HVAC_Community_Events::init() - Before new HVAC_Registration()'); // Updated Log
+ new HVAC_Registration(); // Instantiate Registration class to register shortcode
+ error_log('[HVAC DEBUG] HVAC_Community_Events::init() - After new HVAC_Registration()'); // Updated Log
+
+ // Prevent trainers from accessing wp-admin
+ add_action('admin_init', array($this, 'redirect_trainers_from_admin'));
+
+ $initialized = true; // Mark as initialized
+ }
+
+ /**
+ * Redirect HVAC trainers from admin area to frontend dashboard
+ */
+ public function redirect_trainers_from_admin() {
+ if (defined('DOING_AJAX') && DOING_AJAX) {
+ return;
+ }
+
+ // Check if user is trying to access wp-admin and has trainer role but not admin caps
+ if ( is_admin() && ! current_user_can('manage_options') && current_user_can('view_hvac_dashboard') ) {
+ wp_redirect(home_url('/hvac-dashboard/')); // Corrected slug
+ exit;
+ }
+ }
+
+ /**
+ * Load custom templates for plugin pages.
+ *
+ * @param string $template The path of the template to include.
+ * @return string The path of the template to include.
+ */
+ public function load_custom_templates( $template ) {
+ // Check if we are on the HVAC Dashboard page
+ if ( is_page( 'hvac-dashboard' ) ) {
+ $new_template = HVAC_CE_PLUGIN_DIR . 'templates/template-hvac-dashboard.php';
+ if ( file_exists( $new_template ) ) {
+ return $new_template;
+ }
+ }
+
+ // Add checks for other custom pages here if needed
+
+ return $template;
+ }
+
+} // End class HVAC_Community_Events
\ No newline at end of file
diff --git a/hvac-community-events/includes/class-hvac-dashboard-data.php b/hvac-community-events/includes/class-hvac-dashboard-data.php
new file mode 100644
index 00000000..e1da99be
--- /dev/null
+++ b/hvac-community-events/includes/class-hvac-dashboard-data.php
@@ -0,0 +1,312 @@
+user_id = $user_id;
+ }
+
+ /**
+ * Get the total number of events created by the trainer.
+ *
+ * @return int
+ */
+ public function get_total_events_count() : int {
+ $args = array(
+ 'post_type' => Tribe__Events__Main::POSTTYPE,
+ // 'author' => $this->user_id, // Query by organizer instead
+ 'post_status' => array( 'publish', 'future', 'draft', 'pending', 'private' ),
+ 'posts_per_page' => -1,
+ 'fields' => 'ids', // Only need the count
+ // Restore organizer query
+ 'meta_key' => '_EventOrganizerID',
+ 'meta_value' => $this->user_id,
+ 'meta_compare' => '=', // Explicitly set compare
+ 'meta_type' => 'NUMERIC', // Specify numeric comparison
+ );
+ $query = new WP_Query( $args );
+ return (int) $query->found_posts;
+ }
+
+ /**
+ * Get the number of upcoming events for the trainer.
+ *
+ * @return int
+ */
+ public function get_upcoming_events_count() : int {
+ $today = date( 'Y-m-d H:i:s' );
+ $args = array(
+ 'post_type' => Tribe__Events__Main::POSTTYPE,
+ // 'author' => $this->user_id, // Query by organizer instead
+ 'post_status' => array( 'publish', 'future' ), // Only published or scheduled future events
+ 'posts_per_page' => -1,
+ 'fields' => 'ids', // Only need the count
+ 'meta_query' => array(
+ 'relation' => 'AND', // Combine organizer and date query
+ array(
+ 'key' => '_EventOrganizerID',
+ 'value' => $this->user_id,
+ 'compare' => '=',
+ 'type' => 'NUMERIC', // Specify numeric comparison
+ ),
+ array(
+ 'key' => '_EventStartDate',
+ 'value' => $today,
+ 'compare' => '>=',
+ 'type' => 'DATETIME',
+ ),
+ ),
+ 'orderby' => 'event_date',
+ 'order' => 'ASC',
+ );
+ $query = new WP_Query( $args );
+ return (int) $query->found_posts;
+ }
+
+ /**
+ * Get the number of past events for the trainer.
+ *
+ * @return int
+ */
+ public function get_past_events_count() : int {
+ $today = date( 'Y-m-d H:i:s' );
+ $args = array(
+ 'post_type' => Tribe__Events__Main::POSTTYPE,
+ // 'author' => $this->user_id, // Query by organizer instead
+ 'post_status' => array( 'publish', 'private' ), // Count published or private past events
+ 'posts_per_page' => -1,
+ 'fields' => 'ids', // Only need the count
+ 'meta_query' => array(
+ 'relation' => 'AND', // Combine organizer and date query
+ array(
+ 'key' => '_EventOrganizerID',
+ 'value' => $this->user_id,
+ 'compare' => '=',
+ 'type' => 'NUMERIC', // Specify numeric comparison
+ ),
+ array(
+ 'key' => '_EventEndDate', // Use end date to determine if it's truly past
+ 'value' => $today,
+ 'compare' => '<',
+ 'type' => 'DATETIME',
+ ),
+ ),
+ );
+ $query = new WP_Query( $args );
+ return (int) $query->found_posts;
+ }
+
+ /**
+ * Get the total number of tickets sold across all the trainer's events.
+ *
+ * @return int
+ */
+ public function get_total_tickets_sold() : int {
+ $total_tickets = 0;
+ $args = array(
+ 'post_type' => Tribe__Events__Main::POSTTYPE,
+ // 'author' => $this->user_id, // Query by organizer instead
+ 'post_status' => array( 'publish', 'future', 'draft', 'pending', 'private' ), // Include all statuses for historical data
+ 'posts_per_page' => -1,
+ 'fields' => 'ids', // Only need the IDs
+ 'meta_key' => '_EventOrganizerID',
+ 'meta_value' => $this->user_id,
+ 'meta_compare' => '=', // Explicitly set compare
+ 'meta_type' => 'NUMERIC', // Specify numeric comparison
+ 'meta_compare' => '=', // Explicitly set compare
+ 'meta_type' => 'NUMERIC', // Specify numeric comparison
+ );
+ $event_ids = get_posts( $args );
+
+ if ( ! empty( $event_ids ) ) {
+ foreach ( $event_ids as $event_id ) {
+ // Event Tickets Plus often stores sold count in '_tribe_tickets_sold' meta
+ $sold = get_post_meta( $event_id, '_tribe_tickets_sold', true );
+ if ( is_numeric( $sold ) ) {
+ $total_tickets += (int) $sold;
+ }
+ // Fallback or alternative check if needed (e.g., querying attendee posts)
+ // Depending on the exact ticket plugin setup, this might need adjustment.
+ }
+ }
+
+ return $total_tickets;
+ }
+
+ /**
+ * Get the total revenue generated across all the trainer's events.
+ *
+ * @return float
+ */
+ public function get_total_revenue() : float {
+ $total_revenue = 0.0;
+ $args = array(
+ 'post_type' => Tribe__Events__Main::POSTTYPE,
+ // 'author' => $this->user_id, // Query by organizer instead
+ 'post_status' => array( 'publish', 'future', 'draft', 'pending', 'private' ), // Include all statuses for historical data
+ 'posts_per_page' => -1,
+ 'fields' => 'ids', // Only need the IDs
+ 'meta_key' => '_EventOrganizerID',
+ 'meta_value' => $this->user_id,
+ );
+ $event_ids = get_posts( $args );
+
+ if ( ! empty( $event_ids ) ) {
+ foreach ( $event_ids as $event_id ) {
+ // Event Tickets Plus often stores total revenue in '_tribe_revenue_total' meta
+ $revenue = get_post_meta( $event_id, '_tribe_revenue_total', true );
+ if ( is_numeric( $revenue ) ) {
+ $total_revenue += (float) $revenue;
+ }
+ // Depending on the exact ticket plugin setup, this might need adjustment.
+ }
+ }
+
+ return $total_revenue;
+ }
+
+ /**
+ * Get the annual revenue target set by the trainer.
+ *
+ * @return float|null Returns the target as a float, or null if not set.
+ */
+ public function get_annual_revenue_target() : ?float {
+ $target = get_user_meta( $this->user_id, 'annual_revenue_target', true );
+ return ! empty( $target ) && is_numeric( $target ) ? (float) $target : null;
+ }
+
+ /**
+ * Get the data needed for the events table on the dashboard, filtered by time.
+ *
+ * @param string $filter_time The time period to filter events by ('all', 'upcoming', 'past'). Defaults to 'all'.
+ * @return array An array of event data arrays/objects, each containing keys like: id, status, name, link, date, organizer, capacity, sold, revenue.
+ */
+ public function get_events_table_data( string $filter_time = 'all' ) : array {
+ $events_data = [];
+ $today = date( 'Y-m-d H:i:s' );
+ $meta_query_args = array( // Base meta query for organizer
+ 'relation' => 'AND',
+ array(
+ 'key' => '_EventOrganizerID',
+ 'value' => $this->user_id,
+ 'compare' => '=',
+ 'type' => 'NUMERIC',
+ ),
+ );
+ $orderby = 'meta_value';
+ $order = 'DESC'; // Default: show most recent first
+ $post_status = array( 'publish', 'future', 'draft', 'pending', 'private' ); // Include all relevant statuses
+
+ // Modify meta query based on time filter
+ if ( 'upcoming' === $filter_time ) {
+ $meta_query_args[] = array(
+ 'key' => '_EventStartDate',
+ 'value' => $today,
+ 'compare' => '>=',
+ 'type' => 'DATETIME',
+ );
+ $order = 'ASC'; // Show upcoming events chronologically
+ $post_status = array( 'publish', 'future' ); // Only show published/scheduled upcoming
+ } elseif ( 'past' === $filter_time ) {
+ $meta_query_args[] = array(
+ 'key' => '_EventEndDate', // Use end date for past events
+ 'value' => $today,
+ 'compare' => '<',
+ 'type' => 'DATETIME',
+ );
+ // Keep DESC order for past events
+ $post_status = array( 'publish', 'private' ); // Only show completed published/private past events
+ }
+
+ $args = array(
+ 'post_type' => Tribe__Events__Main::POSTTYPE,
+ // 'author' => $this->user_id, // Querying by organizer via meta_query
+ 'post_status' => $post_status, // Use dynamically set statuses
+ 'posts_per_page' => -1,
+ 'meta_query' => $meta_query_args, // Use the constructed meta query
+ 'orderby' => $orderby, // Use dynamic orderby key
+ 'meta_key' => '_EventStartDate', // Still use start date for ordering key
+ 'order' => $order, // Use dynamic order direction
+ );
+
+ $query = new WP_Query( $args );
+
+ if ( $query->have_posts() ) {
+ while ( $query->have_posts() ) {
+ $query->the_post();
+ $event_id = get_the_ID();
+
+ // Get Capacity - Sum capacity of all tickets for this event
+ $total_capacity = 0;
+ if ( function_exists( 'tribe_get_tickets' ) ) {
+ $tickets = tribe_get_tickets( $event_id );
+ if ( $tickets ) {
+ foreach ( $tickets as $ticket ) {
+ $capacity = $ticket->capacity();
+ // -1 often means unlimited capacity for Tribe Tickets
+ if ( $capacity === -1 ) {
+ $total_capacity = -1; // Mark as unlimited
+ break; // No need to sum further if one is unlimited
+ }
+ if ( is_numeric( $capacity ) ) {
+ $total_capacity += $capacity;
+ }
+ }
+ }
+ }
+
+ $sold = get_post_meta( $event_id, '_tribe_tickets_sold', true );
+ $revenue = get_post_meta( $event_id, '_tribe_revenue_total', true );
+
+ $events_data[] = array(
+ 'id' => $event_id,
+ 'status' => get_post_status( $event_id ),
+ 'name' => get_the_title(),
+ // Return raw data instead of calling TEC functions here
+ 'link' => get_permalink( $event_id ), // Use standard WP permalink
+ 'start_date_ts' => strtotime( get_post_meta( $event_id, '_EventStartDate', true ) ), // Return timestamp
+ 'organizer_id' => (int) get_post_meta( $event_id, '_EventOrganizerID', true ), // Return organizer ID
+ 'capacity' => ( $total_capacity === -1 ) ? 'Unlimited' : (int) $total_capacity,
+ 'sold' => is_numeric( $sold ) ? (int) $sold : 0,
+ 'revenue' => is_numeric( $revenue ) ? (float) $revenue : 0.0,
+ );
+ }
+ wp_reset_postdata(); // Restore original Post Data
+ }
+
+ return $events_data;
+ }
+
+} // End class HVAC_Dashboard_Data
\ No newline at end of file
diff --git a/hvac-community-events/includes/class-hvac-registration.php b/hvac-community-events/includes/class-hvac-registration.php
new file mode 100644
index 00000000..c0b90722
--- /dev/null
+++ b/hvac-community-events/includes/class-hvac-registration.php
@@ -0,0 +1,1066 @@
+';
+ if (!empty($errors['transient'])) echo '
' . esc_html($errors['transient']) . '
';
+ // Nonce errors should ideally be caught in admin-post, but display if somehow set
+ if (!empty($errors['nonce'])) echo '
' . esc_html($errors['nonce']) . '
';
+ error_log('[HVAC REG DEBUG] render_registration_form: Errors before display_form_html: ' . print_r($errors, true));
+ if (!empty($errors['account'])) echo '
' . esc_html($errors['account']) . '
';
+ echo '';
+ }
+
+ // Display the form HTML, passing retrieved errors and submitted data
+ // No success message here anymore, success leads to redirect
+ $this->display_form_html($submitted_data, $errors);
+
+ return ob_get_clean();
+ // --- End Render Form ---
+ }
+
+ /**
+ * Processes the registration form submission via admin-post.
+ * Handles validation, user creation, notifications, and redirects.
+ */
+ public function process_registration_submission() {
+ error_log('[HVAC REG DEBUG] process_registration_submission fired.');
+ $errors = [];
+ $submitted_data = $_POST; // Capture submitted data early for potential repopulation
+ $registration_page_url = home_url('/trainer-registration/'); // Adjust if slug changes
+
+ // --- Verify Nonce ---
+ if (!isset($_POST['hvac_registration_nonce']) || !wp_verify_nonce($_POST['hvac_registration_nonce'], 'hvac_trainer_registration')) {
+ $errors['nonce'] = 'Security check failed. Please try submitting the form again.';
+ error_log('[HVAC REG DEBUG] Nonce check failed in admin-post.');
+ $this->redirect_with_errors($errors, $submitted_data, $registration_page_url);
+ // No need for return/exit here, redirect_with_errors exits.
+ }
+ error_log('[HVAC REG DEBUG] Nonce check passed in admin-post.');
+
+
+ // --- File Upload Handling ---
+ $profile_image_data = null;
+ if (isset($_FILES['profile_image']) && $_FILES['profile_image']['error'] !== UPLOAD_ERR_NO_FILE) {
+ if ($_FILES['profile_image']['error'] === UPLOAD_ERR_OK) {
+ // Check if it's actually an uploaded file
+ if (!is_uploaded_file($_FILES['profile_image']['tmp_name'])) {
+ $errors['profile_image'] = 'File upload error (invalid temp file).';
+ error_log('[HVAC REG DEBUG] Profile image upload error: Not an uploaded file.');
+ } else {
+ $allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
+ // Use wp_check_filetype on the actual file name for extension check
+ // Use finfo_file or getimagesize on tmp_name for actual MIME type check for better security
+ $finfo = finfo_open(FILEINFO_MIME_TYPE);
+ $mime_type = finfo_file($finfo, $_FILES['profile_image']['tmp_name']);
+ finfo_close($finfo);
+
+ if (!in_array($mime_type, $allowed_types)) {
+ $errors['profile_image'] = 'Invalid file type detected (' . esc_html($mime_type) . '). Please upload a JPG, PNG, or GIF.';
+ error_log('[HVAC REG DEBUG] Profile image upload error: Invalid MIME type - ' . $mime_type);
+ } else {
+ $profile_image_data = $_FILES['profile_image']; // Store the whole $_FILES entry
+ error_log('[HVAC REG DEBUG] Profile image seems valid.');
+ }
+ }
+ error_log('[HVAC REG DEBUG] process_registration_submission: Errors after validation merge: ' . print_r($errors, true));
+ } else {
+ $errors['profile_image'] = 'There was an error uploading the profile image. Code: ' . $_FILES['profile_image']['error'];
+ error_log('[HVAC REG DEBUG] Profile image upload error code: ' . $_FILES['profile_image']['error']);
+ error_log('[HVAC REG DEBUG] process_registration_submission: Checking if errors is empty. Result: ' . (empty($errors) ? 'Yes' : 'No'));
+ }
+ }
+ // --- End File Upload Handling ---
+
+ // Validate the rest of the form data
+ $validation_errors = $this->validate_registration($submitted_data);
+ $errors = array_merge($errors, $validation_errors); // Combine file errors and validation errors
+
+ // --- Process if No Errors ---
+ if (empty($errors)) {
+ error_log('[HVAC REG DEBUG] Validation passed in admin-post. Attempting account creation...');
+ $user_id = $this->create_trainer_account($submitted_data, $profile_image_data);
+
+ if (is_wp_error($user_id)) {
+ $errors['account'] = $user_id->get_error_message();
+ error_log('[HVAC REG DEBUG] Account creation WP_Error in admin-post: ' . $user_id->get_error_message());
+ $this->redirect_with_errors($errors, $submitted_data, $registration_page_url);
+ // No need for return/exit here
+ } elseif ($user_id) {
+ error_log('[HVAC REG DEBUG] Account creation SUCCESS in admin-post. User ID: ' . $user_id);
+ error_log('[HVAC REG DEBUG] Sending admin notification...');
+ $this->send_admin_notification($user_id, $submitted_data);
+ // $this->send_user_pending_notification($user_id); // TODO
+
+ // --- Success Redirect ---
+ $success_redirect_url = home_url('/registration-pending/'); // URL from E2E test
+ error_log('[HVAC REG DEBUG] Redirecting to success page: ' . $success_redirect_url);
+ wp_safe_redirect($success_redirect_url);
+ exit; // Important after redirect
+
+ } else {
+ // This case should ideally not happen if wp_insert_user works correctly
+ $errors['account'] = 'An unknown error occurred during registration. Please contact support.';
+ error_log('[HVAC REG DEBUG] Account creation failed silently in admin-post (returned false/0).');
+ $this->redirect_with_errors($errors, $submitted_data, $registration_page_url);
+ // No need for return/exit here
+ }
+ } else {
+ error_log('[HVAC REG DEBUG] Validation errors found in admin-post: ' . print_r($errors, true));
+ $this->redirect_with_errors($errors, $submitted_data, $registration_page_url);
+ // No need for return/exit here
+ }
+ }
+
+ /**
+ * Helper function to store errors/data in transient and redirect back to the form page.
+ error_log('[HVAC REG DEBUG] redirect_with_errors: Preparing transient. Key: ' . $transient_key . ' Data: ' . print_r($transient_data, true));
+ *
+ * @param array $errors Array of error messages.
+ * @param array $data Submitted form data.
+ * @param string $redirect_url The URL to redirect back to.
+ */
+ private function redirect_with_errors($errors, $data, $redirect_url) {
+ $transient_id = uniqid(); // Generate unique ID for transient key
+ $transient_key = self::TRANSIENT_PREFIX . $transient_id;
+ $transient_data = [
+ 'errors' => $errors,
+ 'data' => $data, // Store submitted data to repopulate form
+ ];
+ // Store for 5 minutes
+ set_transient($transient_key, $transient_data, MINUTE_IN_SECONDS * 5);
+
+ // Add query arguments to the redirect URL
+ $redirect_url = add_query_arg([
+ 'reg_error' => '1',
+ 'tid' => $transient_id,
+ ], $redirect_url);
+
+ error_log('[HVAC REG DEBUG] Redirecting back with errors. URL: ' . $redirect_url . ' Transient Key: ' . $transient_key);
+ wp_safe_redirect($redirect_url);
+ exit; // Stop execution after redirect
+ }
+
+
+ /**
+ * Displays the actual form HTML.
+ * Receives submitted data and errors as arguments (potentially retrieved from transient).
+ */
+ private function display_form_html($data = [], $errors = []) {
+ // Ensure $data and $errors are arrays, even if transient failed
+ $data = is_array($data) ? $data : [];
+ $errors = is_array($errors) ? $errors : [];
+ ?>
+
+
HVAC Trainer Registration
+
By submitting this form, you will be creating an account in the Upskill HVAC online event system. Once approved, you will be able to login to the trainer portal to manage your profile and event listings.
+
+
+
+ post_content, 'hvac_trainer_registration')) {
+ wp_enqueue_style(
+ 'hvac-registration-style',
+ HVAC_CE_PLUGIN_URL . 'assets/css/hvac-registration.css', // Ensure this CSS file exists and is styled
+ array(),
+ HVAC_CE_VERSION
+ );
+
+ wp_enqueue_script(
+ 'hvac-registration-js',
+ HVAC_CE_PLUGIN_URL . 'assets/js/hvac-registration.js', // Ensure this JS file exists
+ array('jquery'),
+ HVAC_CE_VERSION,
+ true
+ );
+
+ // Localize script to pass states/provinces data and AJAX URL
+ wp_localize_script('hvac-registration-js', 'hvacRegistrationData', array(
+ 'ajax_url' => admin_url('admin-ajax.php'), // Needed if JS fetches states/provinces via AJAX
+ 'states' => $this->get_us_states(), // Pass US states
+ 'provinces' => $this->get_canadian_provinces(), // Pass CA provinces
+ // Pass other country data if needed, or handle via AJAX
+ ));
+ }
+ }
+
+
+ /**
+ * Handle profile image upload after user is created.
+ * Should be called from within create_trainer_account or similar context.
+ *
+ * @param int $user_id The ID of the user to attach the image to.
+ * @param array $file_data The $_FILES array entry for the uploaded image.
+ * @return int|false Attachment ID on success, false on failure.
+ */
+ private function handle_profile_image_upload($user_id, $file_data) {
+ // Basic validation already done in process_registration_submission
+ if (!$user_id || empty($file_data) || !isset($file_data['tmp_name']) || $file_data['error'] !== UPLOAD_ERR_OK) {
+ error_log('[HVAC REG DEBUG] handle_profile_image_upload called with invalid args or file error.');
+ return false;
+ }
+
+ // These files need to be included as dependencies when on the front-end.
+ require_once(ABSPATH . 'wp-admin/includes/image.php');
+ require_once(ABSPATH . 'wp-admin/includes/file.php');
+ require_once(ABSPATH . 'wp-admin/includes/media.php');
+
+ // Let WordPress handle the upload. It moves the file and creates attachment post.
+ // Pass the $_FILES array key ('profile_image' in this case)
+ $attachment_id = media_handle_upload('profile_image', 0); // 0 means don't attach to a post
+
+ if (is_wp_error($attachment_id)) {
+ // Handle upload error
+ error_log('[HVAC REG DEBUG] Profile image upload error for user ' . $user_id . ': ' . $attachment_id->get_error_message());
+ // Optionally add this error to be displayed to the user via transient?
+ // For now, just fail silently in terms of user feedback, but log it.
+ return false;
+ } else {
+ // Store the attachment ID as user meta
+ update_user_meta($user_id, 'profile_image_id', $attachment_id);
+ error_log('[HVAC REG DEBUG] Profile image uploaded successfully for user ' . $user_id . '. Attachment ID: ' . $attachment_id);
+ return $attachment_id;
+ }
+ }
+
+
+ /**
+ * Validate registration form data
+ *
+ * @param array $data Submitted form data ($_POST).
+ * @return array Array of errors, empty if valid.
+ */
+ public function validate_registration($data) {
+ error_log('[HVAC REG DEBUG] validate_registration: Received data: ' . print_r($data, true));
+ $errors = array();
+
+ // Required field validation
+ $required_fields = [
+ 'user_email' => 'Email',
+ 'user_pass' => 'Password',
+ 'confirm_password' => 'Confirm Password',
+ 'first_name' => 'First Name',
+ 'last_name' => 'Last Name',
+ 'display_name' => 'Display Name',
+ 'description' => 'Biographical Info',
+ 'business_name' => 'Business Name',
+ 'business_phone' => 'Business Phone',
+ 'business_email' => 'Business Email',
+ 'business_description' => 'Business Description',
+ 'user_country' => 'Country',
+ 'user_state' => 'State/Province',
+ 'user_city' => 'City',
+ 'user_zip' => 'Zip/Postal Code',
+ 'create_venue' => 'Create Training Venue Profile selection',
+ 'business_type' => 'Business Type',
+ 'application_details' => 'Application Details',
+ ];
+
+ foreach ($required_fields as $field => $label) {
+ // Use trim to catch spaces-only input
+ if (empty($data[$field]) || trim($data[$field]) === '') {
+ $errors[$field] = $label . ' is required.';
+ }
+ }
+
+ // Required checkbox groups
+ $required_checkboxes = [
+ 'training_audience' => 'Training Audience',
+ 'training_formats' => 'Training Formats',
+ 'training_locations' => 'Training Locations',
+ 'training_resources' => 'Training Resources',
+ ];
+ foreach ($required_checkboxes as $field => $label) {
+ // Check if the key exists and is a non-empty array
+ if (empty($data[$field]) || !is_array($data[$field])) {
+ $errors[$field] = 'Please select at least one option for ' . $label . '.';
+ }
+ }
+
+
+ // Email validation
+ if (!empty($data['user_email']) && !is_email($data['user_email'])) {
+ $errors['user_email'] = 'Please enter a valid email address.';
+ }
+ if (!empty($data['business_email']) && !is_email($data['business_email'])) {
+ $errors['business_email'] = 'Please enter a valid business email address.';
+ }
+
+ // Email exists validation (only if email is valid)
+ if (empty($errors['user_email']) && !empty($data['user_email']) && email_exists($data['user_email'])) {
+ $errors['user_email'] = 'This email address is already registered.';
+ }
+
+ // Password validation
+ if (!empty($data['user_pass'])) {
+ if (strlen($data['user_pass']) < 8) {
+ $errors['user_pass'] = 'Password must be at least 8 characters long.';
+ } elseif (!preg_match('/[A-Z]/', $data['user_pass'])) {
+ $errors['user_pass'] = 'Password must contain at least one uppercase letter.';
+ } elseif (!preg_match('/[a-z]/', $data['user_pass'])) {
+ $errors['user_pass'] = 'Password must contain at least one lowercase letter.';
+ } elseif (!preg_match('/[0-9]/', $data['user_pass'])) {
+ $errors['user_pass'] = 'Password must contain at least one number.';
+ }
+ // Consider adding special character requirement if needed: !preg_match('/[\W_]/', $data['user_pass'])
+ }
+
+ // Confirm password validation (only if password itself is not empty)
+ if (!empty($data['user_pass']) && empty($errors['user_pass'])) {
+ if (empty($data['confirm_password'])) {
+ $errors['confirm_password'] = 'Please confirm your password.';
+ } elseif ($data['user_pass'] !== $data['confirm_password']) {
+ $errors['confirm_password'] = 'Passwords do not match.';
+ }
+ }
+
+
+ // URL validation (optional fields)
+ if (!empty($data['user_url']) && !filter_var($data['user_url'], FILTER_VALIDATE_URL)) {
+ $errors['user_url'] = 'Please enter a valid URL for your personal website.';
+ }
+ if (!empty($data['user_linkedin']) && !filter_var($data['user_linkedin'], FILTER_VALIDATE_URL)) {
+ $errors['user_linkedin'] = 'Please enter a valid URL for your LinkedIn profile.';
+ }
+ if (!empty($data['business_website']) && !filter_var($data['business_website'], FILTER_VALIDATE_URL)) {
+ $errors['business_website'] = 'Please enter a valid URL for your business website.';
+ }
+
+ // State/Province 'Other' validation
+ if (!empty($data['user_country'])) {
+ if ($data['user_country'] !== 'United States' && $data['user_country'] !== 'Canada') {
+ // If country is not US/CA, state *must* be 'Other'
+ if (empty($data['user_state']) || $data['user_state'] !== 'Other') {
+ $errors['user_state'] = 'Please select "Other" for State/Province if your country is not US or Canada.';
+ } elseif (empty($data['user_state_other']) || trim($data['user_state_other']) === '') {
+ // If state is 'Other', the text input must not be empty
+ $errors['user_state_other'] = 'Please enter your state/province.';
+ }
+ } elseif (!empty($data['user_state'])) {
+ // If country is US/CA
+ if ($data['user_state'] === 'Other') {
+ // State cannot be 'Other' if country is US/CA
+ $errors['user_state'] = 'Please select your state/province from the list.';
+ } elseif (empty($errors['user_state'])) { // Only check 'Other' field if state itself is valid
+ // Ensure 'Other' text input is cleared if a valid state is selected
+ // This might be better handled by JS, but add server-side check just in case
+ if (!empty($data['user_state_other'])) {
+ // Maybe log a warning? Or just ignore it. Let's ignore for now.
+ // error_log("[HVAC REG DEBUG] 'Other State' field had data but a valid US/CA state was selected.");
+ }
+ }
+ }
+ }
+
+ error_log('[HVAC REG DEBUG] validate_registration: FINAL errors before return: ' . print_r($errors, true));
+ return $errors;
+ }
+
+
+ /**
+ * Create trainer account and associated data
+ *
+ * @param array $data Sanitized form data.
+ * @param array|null $profile_image_data The $_FILES entry for the profile image, if provided.
+ * @return int|WP_Error User ID on success, WP_Error on failure.
+ */
+ private function create_trainer_account($data, $profile_image_data = null) {
+ // Assume data is already somewhat validated by validate_registration
+ // Perform final sanitization here before insertion
+ $user_email = sanitize_email($data['user_email']);
+ $user_pass = $data['user_pass']; // wp_insert_user handles hashing
+ $first_name = sanitize_text_field($data['first_name']);
+ $last_name = sanitize_text_field($data['last_name']);
+ $display_name = sanitize_text_field($data['display_name']);
+ $user_url = !empty($data['user_url']) ? esc_url_raw($data['user_url']) : '';
+ $description = wp_kses_post($data['description']); // Allow some HTML
+
+ // Generate username from email (ensure uniqueness)
+ $username_base = sanitize_user(substr($user_email, 0, strpos($user_email, '@')), true);
+ if (empty($username_base)) { // Handle cases where email might be weird
+ $username_base = 'trainer';
+ }
+ $username = $username_base;
+ $counter = 1;
+ while (username_exists($username)) {
+ $username = $username_base . $counter;
+ $counter++;
+ if ($counter > 100) { // Safety break
+ return new WP_Error('username_generation', 'Could not generate a unique username.');
+ }
+ }
+
+ // User data array
+ $user_data = array(
+ 'user_login' => $username,
+ 'user_email' => $user_email,
+ 'user_pass' => $user_pass,
+ 'first_name' => $first_name,
+ 'last_name' => $last_name,
+ 'display_name' => $display_name,
+ 'user_url' => $user_url,
+ 'description' => $description,
+ 'role' => 'hvac_trainer' // Assign custom role
+ );
+
+ // Insert the user
+ $user_id = wp_insert_user($user_data);
+
+ // Check for errors
+ if (is_wp_error($user_id)) {
+ error_log('[HVAC REG DEBUG] wp_insert_user failed: ' . $user_id->get_error_message());
+ return $user_id; // Return the WP_Error object
+ }
+
+ error_log('[HVAC REG DEBUG] wp_insert_user success. User ID: ' . $user_id);
+
+ // --- Update User Meta ---
+ // Sanitize all meta values before updating
+ $meta_fields = [
+ 'user_linkedin' => !empty($data['user_linkedin']) ? esc_url_raw($data['user_linkedin']) : '',
+ 'personal_accreditation' => !empty($data['personal_accreditation']) ? sanitize_text_field($data['personal_accreditation']) : '',
+ 'business_name' => sanitize_text_field($data['business_name']),
+ 'business_phone' => sanitize_text_field($data['business_phone']),
+ 'business_email' => sanitize_email($data['business_email']),
+ 'business_website' => !empty($data['business_website']) ? esc_url_raw($data['business_website']) : '',
+ 'business_description' => wp_kses_post($data['business_description']),
+ 'user_country' => sanitize_text_field($data['user_country']),
+ // Use the 'Other' field value if state was 'Other', otherwise use the selected state
+ 'user_state' => ($data['user_state'] === 'Other' && isset($data['user_state_other'])) ? sanitize_text_field($data['user_state_other']) : sanitize_text_field($data['user_state']),
+ 'user_city' => sanitize_text_field($data['user_city']),
+ 'user_zip' => sanitize_text_field($data['user_zip']),
+ 'create_venue' => sanitize_text_field($data['create_venue']), // Should be 'Yes' or 'No'
+ 'business_type' => sanitize_text_field($data['business_type']),
+ 'training_audience' => (!empty($data['training_audience']) && is_array($data['training_audience'])) ? array_map('sanitize_text_field', $data['training_audience']) : [],
+ 'training_formats' => (!empty($data['training_formats']) && is_array($data['training_formats'])) ? array_map('sanitize_text_field', $data['training_formats']) : [],
+ 'training_locations' => (!empty($data['training_locations']) && is_array($data['training_locations'])) ? array_map('sanitize_text_field', $data['training_locations']) : [],
+ 'training_resources' => (!empty($data['training_resources']) && is_array($data['training_resources'])) ? array_map('sanitize_text_field', $data['training_resources']) : [],
+ 'application_details' => wp_kses_post($data['application_details']),
+ 'annual_revenue_target' => !empty($data['annual_revenue_target']) ? intval($data['annual_revenue_target']) : '',
+ 'account_status' => 'pending' // Set initial status
+ ];
+
+ foreach ($meta_fields as $key => $value) {
+ update_user_meta($user_id, $key, $value);
+ }
+ error_log('[HVAC REG DEBUG] User meta updated for user ID: ' . $user_id);
+
+ // --- Handle Profile Image Upload ---
+ // Note: handle_profile_image_upload uses media_handle_upload which expects the key from $_FILES
+ if ($profile_image_data) {
+ error_log('[HVAC REG DEBUG] Attempting profile image upload for user ID: ' . $user_id);
+ // We don't need the return value here unless we want to report specific upload errors
+ $this->handle_profile_image_upload($user_id, $profile_image_data); // Pass the $_FILES entry
+ }
+
+ // --- Create Organizer Profile ---
+ error_log('[HVAC REG DEBUG] Attempting organizer profile creation for user ID: ' . $user_id);
+ $organizer_id = $this->create_organizer_profile($user_id, $meta_fields); // Pass sanitized meta fields
+ if ($organizer_id) {
+ error_log('[HVAC REG DEBUG] Organizer profile created/updated. ID: ' . $organizer_id);
+ update_user_meta($user_id, 'hvac_organizer_id', $organizer_id);
+ } else {
+ error_log('[HVAC REG DEBUG] Organizer profile creation failed for user ID: ' . $user_id);
+ // Consider returning an error if this is critical
+ // return new WP_Error('organizer_creation', 'Failed to create the associated organizer profile.');
+ }
+
+
+ // --- Create Training Venue (if requested) ---
+ if (isset($meta_fields['create_venue']) && $meta_fields['create_venue'] === 'Yes') {
+ error_log('[HVAC REG DEBUG] Attempting venue creation for user ID: ' . $user_id);
+ $venue_id = $this->create_training_venue($user_id, $meta_fields); // Pass sanitized meta fields
+ if ($venue_id) {
+ error_log('[HVAC REG DEBUG] Venue created/updated. ID: ' . $venue_id);
+ update_user_meta($user_id, 'hvac_venue_id', $venue_id);
+ } else {
+ error_log('[HVAC REG DEBUG] Venue creation failed for user ID: ' . $user_id);
+ // Consider returning an error if this is critical
+ // return new WP_Error('venue_creation', 'Failed to create the associated training venue.');
+ }
+ }
+
+ // --- Set Account Status to Pending ---
+ // This is already done via user meta, but could also involve custom capabilities or flags
+ // update_user_meta($user_id, 'account_status', 'pending'); // Redundant if set above
+
+ return $user_id; // Return user ID on success
+ }
+
+
+ /**
+ * Create or update an Organizer profile linked to the user using sanitized data.
+ *
+ * @param int $user_id The user ID.
+ * @param array $meta_data Array of sanitized user meta data.
+ * @return int|false Organizer Post ID on success, false on failure.
+ */
+ private function create_organizer_profile($user_id, $meta_data) {
+ if (!class_exists('Tribe__Events__Main') || !function_exists('tribe_create_organizer')) {
+ error_log('[HVAC REG DEBUG] The Events Calendar function tribe_create_organizer not found.');
+ return false;
+ }
+
+ $organizer_data = array(
+ 'Organizer' => $meta_data['business_name'], // Use sanitized business name
+ 'Phone' => $meta_data['business_phone'],
+ 'Website' => $meta_data['business_website'],
+ 'Email' => $meta_data['business_email'],
+ 'Description' => $meta_data['business_description'],
+ 'post_status' => 'publish', // Publish organizer immediately
+ 'post_author' => $user_id // Associate with the new user
+ );
+
+ // Check if an organizer already exists for this user
+ $existing_organizer_id = get_user_meta($user_id, 'hvac_organizer_id', true);
+
+ if ($existing_organizer_id && get_post_type($existing_organizer_id) === Tribe__Events__Main::ORGANIZER_POST_TYPE) {
+ // Update existing organizer
+ $organizer_data['ID'] = $existing_organizer_id;
+ $organizer_id = tribe_update_organizer($existing_organizer_id, $organizer_data);
+ error_log('[HVAC REG DEBUG] Updated existing organizer ID: ' . $existing_organizer_id);
+ } else {
+ // Create new organizer
+ $organizer_id = tribe_create_organizer($organizer_data);
+ error_log('[HVAC REG DEBUG] Created new organizer.');
+ }
+
+
+ if (is_wp_error($organizer_id)) {
+ error_log('[HVAC REG DEBUG] Error creating/updating organizer: ' . $organizer_id->get_error_message());
+ return false;
+ } elseif (!$organizer_id || $organizer_id === 0) { // Check for 0 as well
+ error_log('[HVAC REG DEBUG] tribe_create/update_organizer returned false or 0.');
+ return false;
+ }
+
+ return (int) $organizer_id;
+ }
+
+
+ /**
+ * Create or update a Venue profile linked to the user using sanitized data.
+ *
+ * @param int $user_id The user ID.
+ * @param array $meta_data Array of sanitized user meta data.
+ * @return int|false Venue Post ID on success, false on failure.
+ */
+ private function create_training_venue($user_id, $meta_data) {
+ if (!class_exists('Tribe__Events__Main') || !function_exists('tribe_create_venue')) {
+ error_log('[HVAC REG DEBUG] The Events Calendar function tribe_create_venue not found.');
+ return false;
+ }
+
+ // Use the already processed state/province from meta
+ $state_province = $meta_data['user_state'];
+
+ $venue_data = array(
+ 'Venue' => $meta_data['business_name'] . ' Training Venue', // Venue name from sanitized meta
+ 'Country' => $meta_data['user_country'],
+ 'Address' => '', // TEC doesn't have a single address line, use City/State/Zip
+ 'City' => $meta_data['user_city'],
+ 'StateProvince' => $state_province,
+ 'State' => $state_province, // Also set State field
+ 'Province' => $state_province, // Also set Province field
+ 'Zip' => $meta_data['user_zip'],
+ 'Phone' => $meta_data['business_phone'],
+ 'Website' => $meta_data['business_website'],
+ 'post_status' => 'publish', // Publish venue immediately
+ 'post_author' => $user_id // Associate with the new user
+ );
+
+ // Check if a venue already exists for this user
+ $existing_venue_id = get_user_meta($user_id, 'hvac_venue_id', true);
+
+ if ($existing_venue_id && get_post_type($existing_venue_id) === Tribe__Events__Main::VENUE_POST_TYPE) {
+ // Update existing venue
+ $venue_data['ID'] = $existing_venue_id;
+ $venue_id = tribe_update_venue($existing_venue_id, $venue_data);
+ error_log('[HVAC REG DEBUG] Updated existing venue ID: ' . $existing_venue_id);
+ } else {
+ // Create new venue
+ $venue_id = tribe_create_venue($venue_data);
+ error_log('[HVAC REG DEBUG] Created new venue.');
+ }
+
+
+ if (is_wp_error($venue_id)) {
+ error_log('[HVAC REG DEBUG] Error creating/updating venue: ' . $venue_id->get_error_message());
+ return false;
+ } elseif (!$venue_id || $venue_id === 0) { // Check for 0 as well
+ error_log('[HVAC REG DEBUG] tribe_create/update_venue returned false or 0.');
+ return false;
+ }
+
+ return (int) $venue_id;
+ }
+
+
+ /**
+ * Send notification email to admin about new registration
+ *
+ * @param int $user_id The ID of the newly registered user.
+ * @param array $data The raw submitted form data (used for notification content).
+ */
+ private function send_admin_notification($user_id, $data) {
+ $admin_email = get_option('admin_email');
+ if (!$admin_email) {
+ error_log('[HVAC REG DEBUG] Admin email not configured. Cannot send notification.');
+ return;
+ }
+
+ $user_info = get_userdata($user_id);
+ if (!$user_info) {
+ error_log('[HVAC REG DEBUG] Could not get user data for notification. User ID: ' . $user_id);
+ return;
+ }
+
+ $subject = sprintf('%s - New HVAC Trainer Registration Pending Approval', get_bloginfo('name'));
+
+ // Use sanitized data for the email body where appropriate
+ $message = "A new HVAC trainer has registered and is awaiting approval.\n\n";
+ $message .= "Details:\n";
+ $message .= "Username: " . $user_info->user_login . "\n";
+ $message .= "Email: " . $user_info->user_email . "\n";
+ // Use the sanitized first/last name from user_info if available, fallback to data
+ $first_name = $user_info->first_name ?: sanitize_text_field($data['first_name']);
+ $last_name = $user_info->last_name ?: sanitize_text_field($data['last_name']);
+ $message .= "Name: " . $first_name . " " . $last_name . "\n";
+ $message .= "Business Name: " . sanitize_text_field($data['business_name']) . "\n"; // Use raw data as meta might not be fully updated yet? Safer to use raw.
+ $message .= "Application Details:\n" . wp_kses_post($data['application_details']) . "\n\n"; // Use wp_kses_post for safety
+
+ // Add link to user profile in admin
+ $profile_link = admin_url('user-edit.php?user_id=' . $user_id);
+ $message .= "Approve/Deny User: " . $profile_link . "\n";
+
+ $headers = array('Content-Type: text/plain; charset=UTF-8');
+
+ if (wp_mail($admin_email, $subject, $message, $headers)) {
+ error_log('[HVAC REG DEBUG] Admin notification email sent successfully to ' . $admin_email);
+ } else {
+ error_log('[HVAC REG DEBUG] Failed to send admin notification email to ' . $admin_email);
+ // Consider adding an error to the transient if email failure is critical?
+ // $errors['notification'] = 'Admin notification failed. Registration complete but please contact support.';
+ }
+ }
+
+ // TODO: Add send_user_pending_notification()
+ // TODO: Add send_user_approved_notification()
+ // TODO: Add send_user_denied_notification()
+
+
+ /**
+ * Get list of countries (simplified)
+ */
+ private function get_country_list() {
+ // In a real application, use a more comprehensive list or library
+ return array(
+ 'US' => 'United States',
+ 'CA' => 'Canada',
+ // Add more countries as needed
+ 'GB' => 'United Kingdom',
+ 'AU' => 'Australia',
+ // ...
+ );
+ }
+ /**
+ * Get list of US states
+ */
+ private function get_us_states() {
+ // Use state abbreviations as keys if preferred by JS/validation
+ return array(
+ 'AL' => 'Alabama', 'AK' => 'Alaska', 'AZ' => 'Arizona', 'AR' => 'Arkansas', 'CA' => 'California',
+ 'CO' => 'Colorado', 'CT' => 'Connecticut', 'DE' => 'Delaware', 'DC' => 'District of Columbia', 'FL' => 'Florida',
+ 'GA' => 'Georgia', 'HI' => 'Hawaii', 'ID' => 'Idaho', 'IL' => 'Illinois', 'IN' => 'Indiana',
+ 'IA' => 'Iowa', 'KS' => 'Kansas', 'KY' => 'Kentucky', 'LA' => 'Louisiana', 'ME' => 'Maine',
+ 'MD' => 'Maryland', 'MA' => 'Massachusetts', 'MI' => 'Michigan', 'MN' => 'Minnesota', 'MS' => 'Mississippi',
+ 'MO' => 'Missouri', 'MT' => 'Montana', 'NE' => 'Nebraska', 'NV' => 'Nevada', 'NH' => 'New Hampshire',
+ 'NJ' => 'New Jersey', 'NM' => 'New Mexico', 'NY' => 'New York', 'NC' => 'North Carolina', 'ND' => 'North Dakota',
+ 'OH' => 'Ohio', 'OK' => 'Oklahoma', 'OR' => 'Oregon', 'PA' => 'Pennsylvania', 'RI' => 'Rhode Island',
+ 'SC' => 'South Carolina', 'SD' => 'South Dakota', 'TN' => 'Tennessee', 'TX' => 'Texas', 'UT' => 'Utah',
+ 'VT' => 'Vermont', 'VA' => 'Virginia', 'WA' => 'Washington', 'WV' => 'West Virginia', 'WI' => 'Wisconsin',
+ 'WY' => 'Wyoming'
+ );
+ }
+
+ /**
+ * Get list of Canadian provinces
+ */
+ private function get_canadian_provinces() {
+ // Use province abbreviations as keys if preferred by JS/validation
+ return array(
+ 'AB' => 'Alberta', 'BC' => 'British Columbia', 'MB' => 'Manitoba', 'NB' => 'New Brunswick',
+ 'NL' => 'Newfoundland and Labrador', 'NS' => 'Nova Scotia', 'ON' => 'Ontario', 'PE' => 'Prince Edward Island',
+ 'QC' => 'Quebec', 'SK' => 'Saskatchewan', 'NT' => 'Northwest Territories', 'NU' => 'Nunavut', 'YT' => 'Yukon'
+ );
+ }
+
+} // End class HVAC_Registration
\ No newline at end of file
diff --git a/hvac-community-events/includes/class-hvac-roles.php b/hvac-community-events/includes/class-hvac-roles.php
new file mode 100644
index 00000000..74bad34e
--- /dev/null
+++ b/hvac-community-events/includes/class-hvac-roles.php
@@ -0,0 +1,93 @@
+get_trainer_capabilities()
+ );
+ }
+
+ /**
+ * Remove the hvac_trainer role
+ */
+ public function remove_trainer_role() {
+ remove_role('hvac_trainer');
+ }
+
+ /**
+ * Get all capabilities for the trainer role
+ */
+ public function get_trainer_capabilities() {
+ $caps = array(
+ // Basic WordPress capabilities
+ 'read' => true,
+ 'upload_files' => true,
+
+ // Custom HVAC capabilities
+ 'manage_hvac_events' => true,
+ 'edit_hvac_profile' => true,
+ 'view_hvac_dashboard' => true,
+ 'manage_attendees' => true,
+ 'email_attendees' => true,
+
+ // The Events Calendar capabilities
+ 'publish_tribe_events' => true,
+ 'edit_tribe_events' => true,
+ 'delete_tribe_events' => true,
+ 'edit_published_tribe_events' => true,
+ 'delete_published_tribe_events' => true,
+ 'read_private_tribe_events' => true,
+ );
+
+ // Explicitly deny admin capabilities
+ $denied_caps = array(
+ 'manage_options',
+ 'moderate_comments',
+ 'manage_categories',
+ 'manage_links',
+ 'edit_others_posts',
+ 'edit_pages',
+ 'edit_others_pages',
+ 'edit_published_pages',
+ 'publish_pages',
+ 'delete_pages',
+ 'delete_others_pages',
+ 'delete_published_pages',
+ 'delete_others_posts',
+ 'import',
+ 'export',
+ 'edit_theme_options',
+ );
+
+ foreach ($denied_caps as $cap) {
+ $caps[$cap] = false;
+ }
+
+ return $caps;
+ }
+
+ /**
+ * Check if current user has a specific HVAC trainer capability
+ */
+ public static function check_trainer_capability($capability) {
+ return current_user_can($capability);
+ }
+}
\ No newline at end of file
diff --git a/hvac-community-events/includes/class-hvac-settings.php b/hvac-community-events/includes/class-hvac-settings.php
new file mode 100644
index 00000000..5fac5437
--- /dev/null
+++ b/hvac-community-events/includes/class-hvac-settings.php
@@ -0,0 +1,71 @@
+' . __('Configure settings for HVAC Community Events', 'hvac-ce') . '
';
+ }
+
+ public function notification_emails_callback() {
+ $options = get_option('hvac_ce_options');
+ echo '';
+ echo '
' .
+ __('Comma-separated list of emails to notify when new trainers register', 'hvac-ce') . '
';
+ }
+
+ public function options_page() {
+ ?>
+
+
+
+
+ init();
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Initialize hooks.
+ */
+ public function init() {
+ // REMOVED: Hooks for processing form submissions (admin_post_hvac_save_event)
+ // add_action( 'admin_post_hvac_save_event', [ $this, 'process_event_submission' ] );
+ // add_action( 'admin_post_nopriv_hvac_save_event', [ $this, 'process_event_submission' ] ); // Handle non-logged-in attempts if necessary
+
+ // REMOVED: Shortcode registration for [hvac_event_form]
+ // add_shortcode( 'hvac_event_form', [ $this, 'display_event_form_shortcode' ] );
+ }
+
+ // REMOVED: display_event_form_shortcode method as we will link to the default TEC CE form page.
+
+ // REMOVED: process_event_submission method as TEC CE shortcode handles its own submission.
+
+ // REMOVED: can_user_edit_event helper method as it's no longer used.
+
+}
+
+// Instantiate the class
+HVAC_Event_Handler::get_instance();
\ No newline at end of file
diff --git a/hvac-community-events/includes/community/class-event-summary-data.php b/hvac-community-events/includes/community/class-event-summary-data.php
new file mode 100644
index 00000000..4f5dab1a
--- /dev/null
+++ b/hvac-community-events/includes/community/class-event-summary-data.php
@@ -0,0 +1,227 @@
+event_id = absint( $event_id );
+ if ( $this->event_id > 0 ) {
+ $this->event_post = get_post( $this->event_id );
+ // Ensure it's an event post type (adjust post type if needed)
+ if ( ! $this->event_post || get_post_type( $this->event_post ) !== Tribe__Events__Main::POSTTYPE ) {
+ $this->event_id = null;
+ $this->event_post = null;
+ }
+ }
+ }
+
+ /**
+ * Check if the event is valid.
+ *
+ * @return bool True if the event ID is valid and the post exists, false otherwise.
+ */
+ public function is_valid_event() {
+ return ! is_null( $this->event_post );
+ }
+
+ /**
+ * Get basic event details.
+ *
+ * @return array|null An array of event details or null if the event is invalid.
+ */
+ public function get_event_details() {
+ if ( ! $this->is_valid_event() ) {
+ return null;
+ }
+
+ $details = [
+ 'id' => $this->event_id,
+ 'title' => get_the_title( $this->event_id ),
+ 'description' => apply_filters( 'the_content', get_post_field( 'post_content', $this->event_id ) ),
+ 'excerpt' => get_the_excerpt( $this->event_id ),
+ 'permalink' => get_permalink( $this->event_id ),
+ 'start_date' => null,
+ 'end_date' => null,
+ 'cost' => null,
+ 'is_all_day' => false,
+ 'is_recurring'=> false,
+ 'timezone' => null,
+ ];
+
+ // Use TEC functions if available
+ if ( function_exists( 'tribe_get_start_date' ) ) {
+ $details['start_date'] = tribe_get_start_date( $this->event_id, true, 'Y-m-d H:i:s' ); // Get raw date/time
+ }
+ if ( function_exists( 'tribe_get_end_date' ) ) {
+ $details['end_date'] = tribe_get_end_date( $this->event_id, true, 'Y-m-d H:i:s' ); // Get raw date/time
+ }
+ if ( function_exists( 'tribe_get_cost' ) ) {
+ $details['cost'] = tribe_get_cost( $this->event_id, true );
+ }
+ if ( function_exists( 'tribe_event_is_all_day' ) ) {
+ $details['is_all_day'] = tribe_event_is_all_day( $this->event_id );
+ }
+ if ( function_exists( 'tribe_is_recurring_event' ) ) {
+ $details['is_recurring'] = tribe_is_recurring_event( $this->event_id );
+ }
+ if ( function_exists( 'tribe_get_timezone' ) ) {
+ $details['timezone'] = tribe_get_timezone( $this->event_id );
+ }
+
+ return $details;
+ }
+
+ /**
+ * Get event venue details.
+ *
+ * @return array|null An array of venue details or null if the event is invalid or has no venue.
+ */
+ public function get_event_venue_details() {
+ if ( ! $this->is_valid_event() ) {
+ return null;
+ }
+
+ $venue_details = null;
+ $venue_id = null;
+
+ if ( function_exists( 'tribe_get_venue_id' ) ) {
+ $venue_id = tribe_get_venue_id( $this->event_id );
+ }
+
+ if ( $venue_id && function_exists( 'tribe_get_venue_details' ) ) {
+ // tribe_get_venue_details is deprecated, use individual functions
+ $venue_details = [
+ 'id' => $venue_id,
+ 'name' => function_exists('tribe_get_venue') ? tribe_get_venue( $venue_id ) : get_the_title( $venue_id ),
+ 'address' => function_exists('tribe_get_full_address') ? tribe_get_full_address( $venue_id ) : null,
+ 'street' => function_exists('tribe_get_address') ? tribe_get_address( $venue_id ) : null,
+ 'city' => function_exists('tribe_get_city') ? tribe_get_city( $venue_id ) : null,
+ 'stateprovince' => function_exists('tribe_get_stateprovince') ? tribe_get_stateprovince( $venue_id ) : null, // Use stateprovince for consistency
+ 'state' => function_exists('tribe_get_state') ? tribe_get_state( $venue_id ) : null,
+ 'province' => function_exists('tribe_get_province') ? tribe_get_province( $venue_id ) : null,
+ 'zip' => function_exists('tribe_get_zip') ? tribe_get_zip( $venue_id ) : null,
+ 'country' => function_exists('tribe_get_country') ? tribe_get_country( $venue_id ) : null,
+ 'phone' => function_exists('tribe_get_phone') ? tribe_get_phone( $venue_id ) : null,
+ 'website' => function_exists('tribe_get_venue_website_link') ? tribe_get_venue_website_link( $venue_id, false ) : null, // Get URL only
+ 'map_link' => function_exists('tribe_get_map_link') ? tribe_get_map_link( $venue_id ) : null,
+ 'directions_link' => function_exists('tribe_get_directions_link') ? tribe_get_directions_link( $venue_id ) : null,
+ ];
+ }
+
+ return $venue_details;
+ }
+
+ /**
+ * Get event organizer details.
+ *
+ * @return array|null An array of organizer details or null if the event is invalid or has no organizer.
+ */
+ public function get_event_organizer_details() {
+ if ( ! $this->is_valid_event() ) {
+ return null;
+ }
+
+ $organizer_details = null;
+ $organizer_ids = [];
+
+ if ( function_exists( 'tribe_get_organizer_ids' ) ) {
+ $organizer_ids = tribe_get_organizer_ids( $this->event_id );
+ }
+
+ // Get details for the first organizer found
+ if ( ! empty( $organizer_ids ) && is_array( $organizer_ids ) ) {
+ $organizer_id = $organizer_ids[0];
+
+ if ( $organizer_id > 0 ) {
+ $organizer_details = [
+ 'id' => $organizer_id,
+ 'name' => function_exists('tribe_get_organizer') ? tribe_get_organizer( $organizer_id ) : get_the_title( $organizer_id ),
+ 'phone' => function_exists('tribe_get_organizer_phone') ? tribe_get_organizer_phone( $organizer_id ) : null,
+ 'website' => function_exists('tribe_get_organizer_website_link') ? tribe_get_organizer_website_link( $organizer_id, false ) : null, // Get URL only
+ 'email' => function_exists('tribe_get_organizer_email') ? tribe_get_organizer_email( $organizer_id ) : null,
+ 'permalink' => function_exists('tribe_get_event_link') ? tribe_get_event_link( $organizer_id, false, false ) : get_permalink( $organizer_id ), // Link to organizer post
+ ];
+ }
+ }
+
+ return $organizer_details;
+ }
+
+ /**
+ * Get transaction data associated with the event.
+ * Requires Event Tickets / Event Tickets Plus.
+ *
+ * @return array An array of transaction data (e.g., orders, attendees). Empty array if none or invalid event.
+ */
+ public function get_event_transactions() {
+ if ( ! $this->is_valid_event() ) {
+ return [];
+ }
+
+ $transactions = [];
+
+ // Check if Event Tickets is active and the necessary class/method exists
+ if ( class_exists( 'Tribe__Tickets__Tickets_Handler' ) && method_exists( Tribe__Tickets__Tickets_Handler::instance(), 'get_attendees_by_id' ) ) {
+ $attendees = Tribe__Tickets__Tickets_Handler::instance()->get_attendees_by_id( $this->event_id );
+
+ if ( is_array( $attendees ) ) {
+ foreach ( $attendees as $attendee ) {
+ // Extract relevant data - structure might vary based on ticket provider (Woo, EDD, RSVP, Tribe)
+ $order_id = isset( $attendee['order_id'] ) ? $attendee['order_id'] : null;
+ $ticket_type_id = isset( $attendee['product_id'] ) ? $attendee['product_id'] : null; // product_id often holds ticket type ID
+ $attendee_id = isset( $attendee['attendee_id'] ) ? $attendee['attendee_id'] : null; // Unique ID for the attendee record
+
+ // Get purchaser info (might be stored differently depending on provider)
+ $purchaser_name = isset( $attendee['holder_name'] ) ? $attendee['holder_name'] : null;
+ $purchaser_email = isset( $attendee['holder_email'] ) ? $attendee['holder_email'] : null;
+ if ( empty( $purchaser_name ) && isset( $attendee['purchaser_name'] ) ) {
+ $purchaser_name = $attendee['purchaser_name'];
+ }
+ if ( empty( $purchaser_email ) && isset( $attendee['purchaser_email'] ) ) {
+ $purchaser_email = $attendee['purchaser_email'];
+ }
+
+
+ $transactions[] = [
+ 'attendee_id' => $attendee_id,
+ 'order_id' => $order_id,
+ 'ticket_type_id' => $ticket_type_id,
+ 'ticket_type_name'=> $ticket_type_id ? get_the_title( $ticket_type_id ) : 'N/A',
+ 'purchaser_name' => $purchaser_name,
+ 'purchaser_email' => $purchaser_email,
+ 'security_code' => isset( $attendee['security_code'] ) ? $attendee['security_code'] : null,
+ 'checked_in' => isset( $attendee['check_in'] ) ? (bool) $attendee['check_in'] : false,
+ // Add other relevant fields if needed, e.g., price, order date
+ ];
+ }
+ }
+ }
+
+ return $transactions;
+ }
+}
\ No newline at end of file
diff --git a/hvac-community-events/includes/community/class-hvac-profile.php b/hvac-community-events/includes/community/class-hvac-profile.php
new file mode 100644
index 00000000..0c5099b6
--- /dev/null
+++ b/hvac-community-events/includes/community/class-hvac-profile.php
@@ -0,0 +1,979 @@
+ID : null;
+
+ if ($profile_page_id && is_page($profile_page_id) && isset($_GET['profile_updated'])) {
+ // Check if the redirect is simply adding/removing a trailing slash to our success URL
+ // Allow this specific type of canonical redirect to proceed.
+ $success_url_base = home_url('/trainer-profile/');
+ $success_url_with_flag = add_query_arg('profile_updated', '1', $success_url_base);
+
+ // Compare URLs ignoring the trailing slash
+ if (untrailingslashit($redirect_url) === untrailingslashit($success_url_with_flag)) {
+ return $redirect_url; // Allow this specific redirect
+ }
+ // Otherwise, prevent any other canonical redirect on this specific request
+ // error_log("[DEBUG CANONICAL] Preventing redirect from {$requested_url} to {$redirect_url}"); // Optional debug
+ return false;
+ }
+ return $redirect_url; // Allow all other canonical redirects
+ }
+
+
+ /**
+ * Checks if the profile form was submitted and processes it.
+ * Hooked to wp_loaded.
+ */
+ public function maybe_process_profile_submission() {
+ if (isset($_POST['action']) && $_POST['action'] === self::PROFILE_ACTION) {
+ $this->process_profile_submission();
+ }
+ }
+
+
+ /**
+ * Enqueues styles and scripts. Reuses registration form styles.
+ */
+ public function enqueue_scripts() {
+ error_log('[DEBUG HVAC_Profile::enqueue_scripts] Method called.'); // Log method entry
+ // Only enqueue on pages where the shortcode might be present
+ // A more robust check might involve checking post content or using a flag
+ $profile_page_object = get_page_by_path('trainer-profile', OBJECT, 'page');
+ $profile_page_id = $profile_page_object ? $profile_page_object->ID : null;
+ error_log('[DEBUG HVAC_Profile::enqueue_scripts] Profile Page ID found: ' . print_r($profile_page_id, true)); // Log page ID result
+ error_log('[DEBUG HVAC_Profile::enqueue_scripts] is_page check result: ' . (is_page($profile_page_id) ? 'true' : 'false')); // Log is_page result
+
+ // More robust check: See if the current post content contains our shortcode
+ global $post;
+ $should_enqueue = false;
+ if (is_a($post, 'WP_Post') && has_shortcode($post->post_content, 'hvac_trainer_profile')) {
+ $should_enqueue = true;
+ }
+ error_log('[DEBUG HVAC_Profile::enqueue_scripts] Shortcode check result: ' . ($should_enqueue ? 'true' : 'false')); // Log shortcode check
+
+ // if ($profile_page_id && is_page($profile_page_id)) { // Original check
+ if ($should_enqueue) { // Use shortcode check instead
+ error_log('[DEBUG HVAC_Profile::enqueue_scripts] Condition met, enqueuing scripts/styles.'); // Log condition met
+ wp_enqueue_style(
+ 'hvac-registration-style',
+ HVAC_CE_PLUGIN_URL . 'assets/css/hvac-registration.css',
+ array(), // Dependencies
+ HVAC_CE_VERSION
+ );
+ wp_enqueue_script(
+ 'hvac-registration-script',
+ HVAC_CE_PLUGIN_URL . 'assets/js/hvac-registration.js',
+ array('jquery'), // Dependencies
+ HVAC_CE_VERSION,
+ true // Load in footer
+ );
+ // Pass country/state data to JS (same as registration)
+ $countries = $this->get_country_list();
+ $us_states = $this->get_us_states();
+ $ca_provinces = $this->get_canadian_provinces();
+ // Add debug logging
+ error_log('[DEBUG HVAC_Profile::enqueue_scripts] Countries: ' . print_r($countries, true));
+ error_log('[DEBUG HVAC_Profile::enqueue_scripts] US States: ' . print_r($us_states, true));
+ error_log('[DEBUG HVAC_Profile::enqueue_scripts] CA Provinces: ' . print_r($ca_provinces, true));
+
+ wp_localize_script('hvac-registration-script', 'hvac_reg_vars', array(
+ 'ajax_url' => admin_url('admin-ajax.php'), // If needed for future AJAX
+ 'countries' => $countries,
+ 'us_states' => $us_states,
+ 'ca_provinces' => $ca_provinces,
+ 'selected_country' => '', // Will be populated dynamically if needed
+ 'selected_state' => '' // Will be populated dynamically if needed
+ ));
+ }
+ }
+
+
+ /**
+ * Renders the profile form shortcode.
+ * Handles security checks and retrieves messages from transients.
+ */
+ public function render_profile_form() {
+ error_log('[DEBUG PROFILE RENDER] Entering render_profile_form'); // Log entry
+ // Ensure instance exists (runs constructor and adds hooks) when shortcode is rendered
+ self::instance(); // Restore instantiation via shortcode render
+
+ // --- Security Check ---
+ if (!is_user_logged_in() || !current_user_can('edit_hvac_profile')) { // Use appropriate capability
+ return '
You must be logged in as a trainer to view this page.
'; // Append to output
+ foreach ($errors as $error_key => $error_message) {
+ $output .= '
Error: ' . esc_html($error_message) . '
'; // Append to output
+ }
+ $output .= '
'; // Append to output
+ }
+ }
+
+ // Display the form HTML (needs to return instead of echo now)
+ $output .= $this->display_profile_form_html($user_id, $errors, $submitted_data); // <<< Pass submitted_data, append output
+
+ error_log('[DEBUG PROFILE RENDER] Exiting render_profile_form'); // Log exit
+ return $output; // Return the built string
+ }
+
+ /**
+ * Displays the actual profile form HTML.
+ * NOW RETURNS HTML STRING INSTEAD OF ECHOING.
+ *
+ * @param int $user_id The ID of the current user.
+ * @param array $errors Array of validation errors.
+ * @param array $submitted_data Array of submitted form data (used for repopulation on error).
+ * @return string Form HTML.
+ */
+ private function display_profile_form_html($user_id, $errors = [], $submitted_data = []) { // <<< Added $submitted_data param
+ error_log('[DEBUG PROFILE RENDER] Entering display_profile_form_html for user ID: ' . $user_id); // Log entry
+ $current_user = get_userdata($user_id);
+ if (!$current_user) {
+ return '
Error: Could not load user data.
'; // Return instead of echo
+ }
+
+ // Fetch user meta data - adapt keys from registration logic
+ $meta_keys = [
+ 'first_name', 'last_name', 'description', // Core WP fields
+ 'user_url', // Core WP field
+ 'user_linkedin', 'personal_accreditation', 'business_name',
+ 'business_phone', 'business_email', 'business_website', 'business_description',
+ 'user_country', 'user_state', 'user_city', 'user_zip',
+ 'business_type', 'training_audience', 'training_formats',
+ 'training_locations', 'training_resources',
+ 'profile_image_id', // Store attachment ID instead of URL
+ 'linked_venue_id' // Store linked venue post ID
+ ];
+ $user_meta = [];
+ foreach ($meta_keys as $key) {
+ // Core WP fields are directly on the user object
+ if (isset($current_user->$key)) {
+ $user_meta[$key] = $current_user->$key;
+ } else {
+ $user_meta[$key] = get_user_meta($user_id, $key, true);
+ }
+ }
+ // Special handling for user_email and display_name from user object
+ $user_meta['user_email'] = $current_user->user_email;
+ $user_meta['display_name'] = $current_user->display_name;
+
+ // Prioritize submitted data (if available, e.g., after error) over stored meta for repopulation
+ $form_data = !empty($submitted_data) ? $submitted_data : $user_meta; // <<< Use submitted data if present
+
+ // Ensure array types for checkboxes/multiselects using the correct data source
+ $array_keys = ['training_audience', 'training_formats', 'training_locations', 'training_resources'];
+ foreach($array_keys as $key) {
+ if (!isset($form_data[$key]) || !is_array($form_data[$key])) { // Check form_data
+ $form_data[$key] = []; // Default to empty array if not set or not array
+ }
+ }
+
+ // Get profile image URL (still based on stored meta)
+ $profile_image_url = '';
+ if (!empty($user_meta['profile_image_id']) && is_numeric($user_meta['profile_image_id'])) {
+ $profile_image_url = wp_get_attachment_image_url((int)$user_meta['profile_image_id'], 'thumbnail'); // Or another appropriate size
+ }
+
+ // Get linked venue info (still based on stored meta)
+ $linked_venue_id = $user_meta['linked_venue_id'] ?? null;
+ $venue_info = '';
+ if ($linked_venue_id && get_post_status($linked_venue_id) === 'publish') {
+ $venue_title = get_the_title($linked_venue_id);
+ $venue_url = get_permalink($linked_venue_id); // Or admin edit link if preferred
+ $venue_info = 'Linked Training Venue: ' . esc_html($venue_title) . '';
+ // TODO: Add more venue details if needed (address etc.)
+ } elseif ($linked_venue_id) {
+ $venue_info = 'Linked Training Venue: (Venue not published or found)';
+ } else {
+ $venue_info = 'No Training Venue linked to this profile.';
+ }
+
+ // Start building HTML string
+ $html = '
'; // Reuse registration form class
+ $html .= '
Edit Trainer Profile
';
+
+ $html .= '';
+ $html .= '
'; // End .hvac-profile-form
+
+ error_log('[DEBUG PROFILE RENDER] Exiting display_profile_form_html'); // Log exit
+ return $html; // Return the built string
+ }
+
+
+ /**
+ * Processes the profile form submission.
+ * Handles validation, user update, and redirects.
+ */
+ public function process_profile_submission() {
+ // Verify nonce
+ if (!isset($_POST['hvac_profile_nonce']) || !wp_verify_nonce($_POST['hvac_profile_nonce'], self::PROFILE_ACTION)) {
+ wp_die('Security check failed.');
+ }
+
+ // Check user logged in
+ if (!is_user_logged_in()) {
+ wp_die('You must be logged in to update your profile.');
+ }
+ $user_id = get_current_user_id();
+
+ // Check capability
+ if (!current_user_can('edit_hvac_profile', $user_id)) { // Check capability for the specific user
+ wp_die('You do not have permission to edit this profile.');
+ }
+
+ // Sanitize POST data (basic sanitization, more specific in validation/update)
+ $submitted_data = stripslashes_deep($_POST);
+
+ // Validate data
+ $errors = $this->validate_profile_data($submitted_data, $user_id);
+
+ // Redirect back with errors if validation fails
+ if (!empty($errors)) {
+ $redirect_url = home_url('/trainer-profile/'); // Redirect back to profile page
+ $this->redirect_with_errors($errors, $redirect_url, $submitted_data); // <<< Pass submitted data
+ exit; // Stop execution after redirect
+ }
+
+ // Handle image upload/deletion
+ $image_data = $_FILES['profile_image'] ?? null;
+ $delete_image = isset($submitted_data['delete_profile_image']) && $submitted_data['delete_profile_image'] === '1';
+
+ // Attempt to update the user account
+ $update_result = $this->update_trainer_account($user_id, $submitted_data, $image_data, $delete_image);
+
+ // Handle update result
+ if (is_wp_error($update_result)) {
+ // Update failed, redirect back with error message(s)
+ $errors = $update_result->get_error_messages(); // Get all error messages
+ $error_codes = $update_result->get_error_codes();
+ $error_map = [];
+ foreach($error_codes as $index => $code) {
+ $error_map[$code] = $errors[$index]; // Map code to message
+ }
+ $redirect_url = home_url('/trainer-profile/');
+ $this->redirect_with_errors($error_map, $redirect_url, $submitted_data); // Pass error map and submitted data
+ exit;
+ } elseif ($update_result === true) {
+ // Success! Redirect with success flag
+ $redirect_url = add_query_arg('profile_updated', '1', home_url('/trainer-profile/'));
+ wp_safe_redirect($redirect_url);
+ exit;
+ } else {
+ // Should not happen if update_trainer_account always returns true or WP_Error
+ $errors['general'] = 'An unexpected error occurred during profile update.';
+ $redirect_url = home_url('/trainer-profile/');
+ $this->redirect_with_errors($errors, $redirect_url, $submitted_data); // Pass submitted data
+ exit;
+ }
+ }
+
+ /**
+ * Redirects back to a URL with errors stored in a transient.
+ * NOW ACCEPTS SUBMITTED DATA TO STORE IN TRANSIENT.
+ *
+ * @param array $errors Array of errors (key => message).
+ * @param string $redirect_url URL to redirect back to.
+ * @param array $submitted_data The submitted form data.
+ */
+ private function redirect_with_errors($errors, $redirect_url, $submitted_data) { // <<< Added $submitted_data param
+ $transient_id = wp_generate_password(12, false); // Generate a unique ID for the transient
+ $transient_key = self::TRANSIENT_PREFIX . $transient_id;
+
+ // Store both errors and submitted data in the transient
+ $transient_data = [
+ 'errors' => $errors,
+ 'data' => $submitted_data, // <<< Store submitted data
+ ];
+
+ set_transient($transient_key, $transient_data, MINUTE_IN_SECONDS * 5); // Store for 5 minutes
+
+ // Add error flag and transient ID to the redirect URL
+ $redirect_url = add_query_arg(array(
+ 'profile_error' => '1',
+ 'tid' => $transient_id
+ ), $redirect_url);
+
+ wp_safe_redirect($redirect_url);
+ exit;
+ }
+
+
+ /**
+ * Validates the submitted profile data.
+ * Changed from private to public for unit testing.
+ *
+ * @param array $data Submitted form data.
+ * @param int $user_id The ID of the user being updated.
+ * @return array Array of errors (empty if valid).
+ */
+ public function validate_profile_data($data, $user_id) { // Changed from private to public
+ $errors = [];
+ $current_user = get_userdata($user_id);
+
+ // --- Account Information ---
+ // Email
+ if (empty($data['user_email'])) {
+ $errors['user_email'] = 'Email address is required.';
+ } elseif (!is_email($data['user_email'])) {
+ $errors['user_email'] = 'Invalid email address format.';
+ } elseif ($data['user_email'] !== $current_user->user_email && email_exists($data['user_email'])) {
+ $errors['user_email'] = 'This email address is already in use by another account.';
+ }
+
+ // Password (only validate if a new password is provided)
+ if (!empty($data['user_pass'])) {
+ if (strlen($data['user_pass']) < 8) {
+ $errors['user_pass'] = 'Password must be at least 8 characters long.';
+ } elseif (!preg_match('/[A-Z]/', $data['user_pass'])) {
+ $errors['user_pass'] = 'Password must contain at least one uppercase letter.';
+ } elseif (!preg_match('/[a-z]/', $data['user_pass'])) {
+ $errors['user_pass'] = 'Password must contain at least one lowercase letter.';
+ } elseif (!preg_match('/[0-9]/', $data['user_pass'])) {
+ $errors['user_pass'] = 'Password must contain at least one number.';
+ } elseif (empty($data['confirm_password'])) {
+ $errors['confirm_password'] = 'Please confirm your new password.';
+ } elseif ($data['user_pass'] !== $data['confirm_password']) {
+ $errors['confirm_password'] = 'Passwords do not match.';
+ }
+ }
+
+ // Current Password (required only if email or password changes)
+ $email_changed = isset($data['user_email']) && $data['user_email'] !== $current_user->user_email;
+ $password_changed = !empty($data['user_pass']);
+ if (($email_changed || $password_changed) && empty($data['current_password'])) {
+ $errors['current_password'] = 'Current password is required to update email or password.';
+ }
+ // Note: Actual check if current password is correct happens in update_trainer_account
+
+ // --- Personal Information ---
+ if (empty($data['first_name'])) $errors['first_name'] = 'First Name is required.';
+ if (empty($data['last_name'])) $errors['last_name'] = 'Last Name is required.';
+ if (empty($data['display_name'])) $errors['display_name'] = 'Display Name is required.';
+ if (!empty($data['user_url']) && !filter_var($data['user_url'], FILTER_VALIDATE_URL)) $errors['user_url'] = 'Invalid Personal Website URL format.';
+ if (!empty($data['user_linkedin']) && !filter_var($data['user_linkedin'], FILTER_VALIDATE_URL)) $errors['user_linkedin'] = 'Invalid LinkedIn Profile URL format.';
+ if (empty($data['description'])) $errors['description'] = 'Biographical Info is required.';
+
+ // Profile Image Validation (basic)
+ if (isset($_FILES['profile_image']) && $_FILES['profile_image']['error'] === UPLOAD_ERR_OK) {
+ $allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
+ $file_info = wp_check_filetype_and_ext($_FILES['profile_image']['tmp_name'], $_FILES['profile_image']['name']);
+ if (empty($file_info['type']) || !in_array($file_info['type'], $allowed_types)) {
+ $errors['profile_image'] = 'Invalid file type. Please upload a JPG, PNG, or GIF image.';
+ }
+ // Add size check if needed:
+ // if ($_FILES['profile_image']['size'] > MAX_UPLOAD_SIZE) {
+ // $errors['profile_image'] = 'File size exceeds limit.';
+ // }
+ } elseif (isset($_FILES['profile_image']) && $_FILES['profile_image']['error'] !== UPLOAD_ERR_NO_FILE && $_FILES['profile_image']['error'] !== UPLOAD_ERR_OK) {
+ $errors['profile_image'] = 'Error uploading profile image. Code: ' . $_FILES['profile_image']['error'];
+ }
+
+
+ // --- Business Information ---
+ if (empty($data['business_name'])) $errors['business_name'] = 'Business Name is required.';
+ if (empty($data['business_phone'])) $errors['business_phone'] = 'Business Phone is required.';
+ if (empty($data['business_email'])) {
+ $errors['business_email'] = 'Business Email is required.';
+ } elseif (!is_email($data['business_email'])) {
+ $errors['business_email'] = 'Invalid Business Email format.';
+ }
+ if (!empty($data['business_website']) && !filter_var($data['business_website'], FILTER_VALIDATE_URL)) $errors['business_website'] = 'Invalid Business Website URL format.';
+ if (empty($data['business_description'])) $errors['business_description'] = 'Business Description is required.';
+
+ // --- Address Information ---
+ if (empty($data['user_country'])) $errors['user_country'] = 'Country is required.';
+ if (empty($data['user_state'])) {
+ $errors['user_state'] = 'State/Province is required.';
+ } elseif ($data['user_state'] === 'Other' && empty($data['user_state_other'])) {
+ $errors['user_state_other'] = 'Please specify your state/province.';
+ }
+ if (empty($data['user_city'])) $errors['user_city'] = 'City is required.';
+ if (empty($data['user_zip'])) $errors['user_zip'] = 'Zip/Postal Code is required.';
+
+ // --- Training Information ---
+ if (empty($data['business_type'])) $errors['business_type'] = 'Business Type is required.';
+ if (empty($data['training_audience'])) $errors['training_audience'] = 'Please select at least one option for Training Audience.';
+ if (empty($data['training_formats'])) $errors['training_formats'] = 'Please select at least one option for Training Formats.';
+ if (empty($data['training_locations'])) $errors['training_locations'] = 'Please select at least one option for Training Locations.';
+ if (empty($data['training_resources'])) $errors['training_resources'] = 'Please select at least one option for Training Resources.';
+
+ // Ensure checkbox/multiselect data is arrays if submitted (even if empty)
+ $array_keys = ['training_audience', 'training_formats', 'training_locations', 'training_resources'];
+ foreach($array_keys as $key) {
+ if (isset($data[$key]) && !is_array($data[$key])) {
+ $errors[$key] = 'Invalid data format for ' . $key . '.';
+ } elseif (!isset($data[$key])) {
+ // If the field is required and not submitted at all (e.g., disabled JS)
+ if (in_array($key, ['training_audience', 'training_formats', 'training_locations', 'training_resources'])) { // Add other required array fields if any
+ $errors[$key] = 'Please select at least one option for ' . str_replace('_', ' ', ucfirst($key)) . '.';
+ }
+ }
+ }
+
+
+ return $errors;
+ }
+
+
+ /**
+ * Updates the user account and meta data.
+ *
+ * @param int $user_id The ID of the user to update.
+ * @param array $data Sanitized submitted form data.
+ * @param array|null $image_data Uploaded file data from $_FILES['profile_image'].
+ * @param bool $delete_image Whether to delete the current profile image.
+ * @return bool|WP_Error True on success, WP_Error on failure.
+ */
+ private function update_trainer_account($user_id, $data, $image_data, $delete_image) {
+ error_log('[DEBUG UPDATE_ACCOUNT] Entering function for user ID: ' . $user_id);
+ $update_args = array(
+ 'ID' => $user_id,
+ );
+
+ // Sanitize and prepare data for wp_update_user
+ if (isset($data['user_email'])) {
+ $update_args['user_email'] = sanitize_email($data['user_email']);
+ }
+ if (!empty($data['user_pass'])) {
+ $update_args['user_pass'] = $data['user_pass']; // wp_update_user handles hashing
+ }
+ if (isset($data['user_url'])) {
+ $update_args['user_url'] = esc_url_raw($data['user_url']);
+ }
+ if (isset($data['display_name'])) {
+ $update_args['display_name'] = sanitize_text_field($data['display_name']);
+ }
+ if (isset($data['first_name'])) {
+ $update_args['first_name'] = sanitize_text_field($data['first_name']);
+ }
+ if (isset($data['last_name'])) {
+ $update_args['last_name'] = sanitize_text_field($data['last_name']);
+ }
+ if (isset($data['description'])) {
+ $update_args['description'] = sanitize_textarea_field($data['description']);
+ }
+
+ // Check current password if email or password is being changed
+ $current_user = get_userdata($user_id);
+ $needs_current_password_check = false;
+
+ error_log('[DEBUG UPDATE_ACCOUNT] Checking if password or email update is needed.');
+ // Check if password needs updating
+ if (!empty($data['user_pass'])) {
+ error_log('[DEBUG UPDATE_ACCOUNT] New password provided.');
+ $needs_current_password_check = true;
+ }
+ // Check if email needs updating
+ if (isset($data['user_email']) && $data['user_email'] !== $current_user->user_email) {
+ error_log('[DEBUG UPDATE_ACCOUNT] Email change detected: ' . $current_user->user_email . ' -> ' . $data['user_email']);
+ $needs_current_password_check = true;
+ }
+
+ // Check current password if needed
+ if ($needs_current_password_check) {
+ error_log('[DEBUG UPDATE_ACCOUNT] Current password check required.');
+ if (empty($data['current_password'])) {
+ error_log('[DEBUG UPDATE_ACCOUNT] Error: Current password missing for email/password update.');
+ $error = new WP_Error('missing_current_password', 'Current password is required to update email or password.');
+ error_log('[DEBUG UPDATE_ACCOUNT] Returning WP_Error: ' . $error->get_error_code());
+ return $error;
+ }
+ error_log('[DEBUG UPDATE_ACCOUNT] Checking current password...');
+ if (!wp_check_password($data['current_password'], $current_user->user_pass, $user_id)) {
+ error_log('[DEBUG UPDATE_ACCOUNT] Error: Current password check failed for user ID: ' . $user_id);
+ $error = new WP_Error('incorrect_current_password', 'The current password you entered is incorrect.');
+ error_log('[DEBUG UPDATE_ACCOUNT] Returning WP_Error: ' . $error->get_error_code());
+ return $error;
+ }
+ error_log('[DEBUG UPDATE_ACCOUNT] Current password check successful.');
+ }
+
+ // Update the user core data
+ error_log('[DEBUG UPDATE_ACCOUNT] Calling wp_update_user with args: ' . print_r($update_args, true));
+ $result = wp_update_user($update_args);
+
+ if (is_wp_error($result)) {
+ error_log('[DEBUG UPDATE_ACCOUNT] wp_update_user failed: ' . $result->get_error_message());
+ $error = new WP_Error('user_update_failed', 'Could not update user information: ' . $result->get_error_message());
+ error_log('[DEBUG UPDATE_ACCOUNT] Returning WP_Error: ' . $error->get_error_code());
+ return $error; // Return the modified error object
+ }
+ error_log('[DEBUG UPDATE_ACCOUNT] wp_update_user successful for user ID: ' . $user_id);
+
+ // Update user meta data
+ $meta_to_update = [
+ 'user_linkedin' => sanitize_text_field($data['user_linkedin'] ?? ''),
+ 'personal_accreditation' => sanitize_text_field($data['personal_accreditation'] ?? ''),
+ 'business_name' => sanitize_text_field($data['business_name'] ?? ''),
+ 'business_phone' => sanitize_text_field($data['business_phone'] ?? ''),
+ 'business_email' => sanitize_email($data['business_email'] ?? ''),
+ 'business_website' => esc_url_raw($data['business_website'] ?? ''),
+ 'business_description' => sanitize_textarea_field($data['business_description'] ?? ''),
+ 'user_country' => sanitize_text_field($data['user_country'] ?? ''),
+ 'user_state' => sanitize_text_field($data['user_state'] ?? ''), // Includes 'Other' or selected state
+ 'user_state_other' => ($data['user_state'] ?? '') === 'Other' ? sanitize_text_field($data['user_state_other'] ?? '') : '', // Only save if 'Other' is selected
+ 'user_city' => sanitize_text_field($data['user_city'] ?? ''),
+ 'user_zip' => sanitize_text_field($data['user_zip'] ?? ''),
+ 'business_type' => sanitize_text_field($data['business_type'] ?? ''),
+ 'training_audience' => $data['training_audience'] ?? [], // Already validated as array or empty
+ 'training_formats' => $data['training_formats'] ?? [],
+ 'training_locations' => $data['training_locations'] ?? [],
+ 'training_resources' => $data['training_resources'] ?? [],
+ ];
+ error_log('[DEBUG UPDATE_ACCOUNT] Updating user meta: ' . print_r($meta_to_update, true));
+ foreach ($meta_to_update as $key => $value) {
+ update_user_meta($user_id, $key, $value);
+ }
+
+ // Handle profile image upload/deletion
+ if ($delete_image) {
+ $current_image_id = get_user_meta($user_id, 'profile_image_id', true);
+ error_log('[DEBUG UPDATE_ACCOUNT] Deleting profile image. Current ID: ' . $current_image_id);
+ if ($current_image_id) {
+ wp_delete_attachment($current_image_id, true);
+ delete_user_meta($user_id, 'profile_image_id');
+ }
+ } elseif (!empty($image_data['tmp_name'])) {
+ error_log('[DEBUG UPDATE_ACCOUNT] Processing profile image upload: ' . print_r($image_data, true));
+ // Need wp-admin/includes/file.php, wp-admin/includes/image.php, wp-admin/includes/media.php
+ require_once(ABSPATH . 'wp-admin/includes/file.php');
+ require_once(ABSPATH . 'wp-admin/includes/image.php');
+ require_once(ABSPATH . 'wp-admin/includes/media.php');
+
+ // Handle the upload
+ // Note: media_handle_upload() expects the file input name ('profile_image' in this case)
+ $attachment_id = media_handle_upload('profile_image', 0); // 0 means no parent post
+
+ if (is_wp_error($attachment_id)) {
+ error_log('[DEBUG UPDATE_ACCOUNT] Profile image upload failed: ' . $attachment_id->get_error_message());
+ // Optionally return error, or just log it and continue
+ // return new WP_Error('image_upload_failed', 'Could not upload profile image: ' . $attachment_id->get_error_message());
+ } else {
+ error_log('[DEBUG UPDATE_ACCOUNT] Profile image uploaded successfully. Attachment ID: ' . $attachment_id);
+ // Delete old image if exists
+ $current_image_id = get_user_meta($user_id, 'profile_image_id', true);
+ error_log('[DEBUG UPDATE_ACCOUNT] Deleting old profile image. Current ID: ' . $current_image_id);
+ if ($current_image_id && $current_image_id != $attachment_id) {
+ wp_delete_attachment($current_image_id, true);
+ }
+ // Store new attachment ID
+ update_user_meta($user_id, 'profile_image_id', $attachment_id);
+ }
+ }
+
+ // TODO: Implement logic to update linked TEC Organizer/Venue posts if necessary
+ // This might involve fetching the linked venue ID and updating its details based on profile changes.
+
+ error_log('[DEBUG UPDATE_ACCOUNT] Update process completed successfully for user ID: ' . $user_id . '. Returning true.');
+ return true;
+ }
+
+
+ /**
+ * Returns a list of countries.
+ * Could be expanded or loaded from a config/API.
+ *
+ * @return array
+ */
+ private function get_country_list() {
+ return array(
+ 'US' => 'United States',
+ 'CA' => 'Canada',
+ 'GB' => 'United Kingdom',
+ 'AU' => 'Australia',
+ // Add more countries as needed
+ );
+ }
+
+ /**
+ * Returns a list of US states.
+ *
+ * @return array
+ */
+ private function get_us_states() {
+ return array(
+ 'AL' => 'Alabama', 'AK' => 'Alaska', 'AZ' => 'Arizona', 'AR' => 'Arkansas', 'CA' => 'California',
+ 'CO' => 'Colorado', 'CT' => 'Connecticut', 'DE' => 'Delaware', 'DC' => 'District Of Columbia', 'FL' => 'Florida',
+ 'GA' => 'Georgia', 'HI' => 'Hawaii', 'ID' => 'Idaho', 'IL' => 'Illinois', 'IN' => 'Indiana', 'IA' => 'Iowa',
+ 'KS' => 'Kansas', 'KY' => 'Kentucky', 'LA' => 'Louisiana', 'ME' => 'Maine', 'MD' => 'Maryland',
+ 'MA' => 'Massachusetts', 'MI' => 'Michigan', 'MN' => 'Minnesota', 'MS' => 'Mississippi', 'MO' => 'Missouri',
+ 'MT' => 'Montana', 'NE' => 'Nebraska', 'NV' => 'Nevada', 'NH' => 'New Hampshire', 'NJ' => 'New Jersey',
+ 'NM' => 'New Mexico', 'NY' => 'New York', 'NC' => 'North Carolina', 'ND' => 'North Dakota', 'OH' => 'Ohio',
+ 'OK' => 'Oklahoma', 'OR' => 'Oregon', 'PA' => 'Pennsylvania', 'RI' => 'Rhode Island', 'SC' => 'South Carolina',
+ 'SD' => 'South Dakota', 'TN' => 'Tennessee', 'TX' => 'Texas', 'UT' => 'Utah', 'VT' => 'Vermont',
+ 'VA' => 'Virginia', 'WA' => 'Washington', 'WV' => 'West Virginia', 'WI' => 'Wisconsin', 'WY' => 'Wyoming'
+ );
+ }
+
+ /**
+ * Returns a list of Canadian provinces.
+ *
+ * @return array
+ */
+ private function get_canadian_provinces() {
+ return array(
+ 'AB' => 'Alberta', 'BC' => 'British Columbia', 'MB' => 'Manitoba', 'NB' => 'New Brunswick',
+ 'NL' => 'Newfoundland and Labrador', 'NS' => 'Nova Scotia', 'ON' => 'Ontario', 'PE' => 'Prince Edward Island',
+ 'QC' => 'Quebec', 'SK' => 'Saskatchewan', 'NT' => 'Northwest Territories', 'NU' => 'Nunavut', 'YT' => 'Yukon'
+ );
+ }
+
+} // End class HVAC_Profile
diff --git a/hvac-community-events/includes/community/class-login-handler.php b/hvac-community-events/includes/community/class-login-handler.php
new file mode 100644
index 00000000..bbad0a9c
--- /dev/null
+++ b/hvac-community-events/includes/community/class-login-handler.php
@@ -0,0 +1,174 @@
+' . esc_html__( 'Invalid username or password.', 'hvac-community-events' ) . '';
+ }
+
+ // Define variables needed by the template (if any)
+ // $caption = __( 'Please log in to access the trainer area.', 'hvac-community-events' );
+
+ // Include the custom login form template.
+ // Use a helper function to locate the template, allowing theme overrides.
+ $template_path = HVAC_CE_PLUGIN_DIR . 'templates/community/login-form.php'; // Correct constant name
+ if ( file_exists( $template_path ) ) {
+ include $template_path;
+ } else {
+ // Fallback or error message if template is missing
+ echo '
Error: Login form template not found.
';
+ }
+
+ // Return the buffered content.
+ return ob_get_clean();
+ }
+
+ /**
+ * Enqueues scripts and styles for the login page.
+ */
+ public function enqueue_scripts() {
+ global $post;
+
+ // Only enqueue if the shortcode is present on the current page.
+ if ( is_a( $post, 'WP_Post' ) && has_shortcode( $post->post_content, 'hvac_community_login' ) ) {
+ wp_enqueue_style(
+ 'hvac-community-login-style',
+ HVAC_CE_PLUGIN_URL . 'assets/css/community-login.css',
+ array(), // Add dependencies like theme stylesheet if needed
+ HVAC_CE_VERSION
+ );
+ }
+ }
+
+ /**
+ * Handles custom authentication logic (if needed).
+ * Placeholder for Task 2.2.
+ *
+ * @param string $username Username or email address.
+ * @param string $password Password.
+ */
+ public function handle_authentication( &$username, &$password ) {
+ // Custom validation or checks can go here.
+ // For now, rely on default WordPress authentication.
+ }
+
+ /**
+ * Handles redirecting the user back to the custom login page on authentication failure.
+ *
+ * Hooked to 'wp_login_failed'.
+ */
+ public function handle_login_failure() {
+ // Check if the request originated from our custom login page.
+ // This prevents interference with the standard wp-login.php flow if accessed directly.
+ $referrer = wp_get_referer();
+ $login_page_slug = 'community-login'; // The slug of your custom login page
+
+ if ( $referrer && strpos( $referrer, $login_page_slug ) !== false ) {
+ $login_page_url = home_url( '/' . $login_page_slug . '/' );
+ // Redirect back to the custom login page with a failure flag.
+ wp_safe_redirect( add_query_arg( 'login', 'failed', $login_page_url ) );
+ exit;
+ }
+ // If not referred from our custom login page, let WordPress handle the failure (usually redisplays wp-login.php).
+ }
+
+ // REMOVED: Unnecessary redirect_on_login_failure method.
+ // WordPress handles redirecting back to the referring page (our custom login page)
+ // on authentication failure automatically when using wp_login_form().
+ // The 'login_redirect' filter handles the success case.
+
+ /**
+ * Custom redirect logic after successful login.
+ * Placeholder for Task 2.5.
+ * Filters the login redirect URL based on user role.
+ *
+ * @param string $redirect_to The redirect destination URL.
+ * @param string $requested_redirect_to The requested redirect destination URL (if provided).
+ * @param WP_User|WP_Error $user WP_User object if login successful, WP_Error object otherwise.
+ * @return string Redirect URL.
+ */
+ public function custom_login_redirect( $redirect_to, $requested_redirect_to, $user ) {
+ // Check if login was successful and user is not an error object
+ if ( $user && ! is_wp_error( $user ) ) {
+ // Check if the user has the 'hvac_trainer' role
+ if ( in_array( 'hvac_trainer', (array) $user->roles ) ) {
+ // Redirect HVAC trainers to their dashboard
+ // Assumes dashboard page slug is 'hvac-dashboard'. Adjust if needed.
+ $dashboard_url = home_url( '/hvac-dashboard/' );
+ return $dashboard_url;
+ } else {
+ // For other roles (like admin), redirect to the standard WP admin dashboard.
+ // If $requested_redirect_to is set (e.g., trying to access a specific admin page), respect it.
+ return $requested_redirect_to ? $requested_redirect_to : admin_url();
+ }
+ }
+
+ // If login failed ($user is WP_Error), return the default $redirect_to.
+ // Our redirect_on_login_failure should ideally catch this first, but this is a fallback.
+ return $redirect_to;
+ }
+
+ /**
+ * Redirects logged-in users away from the custom login page.
+ * Hooked to 'template_redirect'.
+ */
+ public function redirect_logged_in_user() {
+ // Check if we are on the custom login page (adjust slug if needed)
+ if ( is_page( 'community-login' ) && is_user_logged_in() ) {
+ // Redirect logged-in users to the dashboard
+ $dashboard_url = home_url( '/hvac-dashboard/' );
+ wp_safe_redirect( $dashboard_url );
+ exit;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/hvac-community-events/templates/community/login-form.php b/hvac-community-events/templates/community/login-form.php
new file mode 100644
index 00000000..04f8a933
--- /dev/null
+++ b/hvac-community-events/templates/community/login-form.php
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+ Invalid username or password.';
+ // }
+ ?>
+
+ true,
+ // 'redirect' is handled by the 'login_redirect' filter in Login_Handler class
+ 'form_id' => 'hvac_community_loginform',
+ 'label_username' => __( 'Username or Email Address', 'hvac-community-events' ),
+ 'label_password' => __( 'Password', 'hvac-community-events' ),
+ 'label_remember' => __( 'Remember Me', 'hvac-community-events' ), // Task 2.3
+ 'label_log_in' => __( 'Log In', 'hvac-community-events' ),
+ 'id_username' => 'user_login',
+ 'id_password' => 'user_pass',
+ 'id_remember' => 'rememberme',
+ 'id_submit' => 'wp-submit',
+ 'remember' => true, // Task 2.3
+ 'value_username' => '',
+ 'value_remember' => false, // Set to true to default the checkbox to checked
+ );
+
+ wp_login_form( $args );
+ ?>
+
+
+
+
+
+ Could not load event summary data.';
+ }
+
+ } else {
+ // Handle case where it's not a tribe_events post or no posts found
+ astra_content_page_loop(); // Fallback to default page loop? Or show error.
+ echo '
Event not found or invalid post type.
';
+ }
+ ?>
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hvac-community-events/templates/template-hvac-dashboard.php b/hvac-community-events/templates/template-hvac-dashboard.php
new file mode 100644
index 00000000..cdd51b93
--- /dev/null
+++ b/hvac-community-events/templates/template-hvac-dashboard.php
@@ -0,0 +1,220 @@
+get_total_events_count();
+$upcoming_events = $dashboard_data->get_upcoming_events_count();
+$past_events = $dashboard_data->get_past_events_count();
+$total_sold = $dashboard_data->get_total_tickets_sold();
+$total_revenue = $dashboard_data->get_total_revenue();
+$revenue_target = $dashboard_data->get_annual_revenue_target();
+
+// --- Template Start ---
+
+get_header(); // Use theme's header
+
+?>
+