Initial commit for Cloudways deployment source (plugin only)

This commit is contained in:
bengizmo 2025-04-07 06:33:21 -03:00
commit 5cae9128b6
18 changed files with 4396 additions and 0 deletions

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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 */
}

View file

@ -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;
}

View file

@ -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 = $('<option></option>').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)
});

View file

@ -0,0 +1,204 @@
<?php
/**
* Plugin Name: HVAC Community Events
* Plugin URI: https://upskillhvac.com
* Description: Custom plugin for HVAC trainer event management system
* Version: 1.0.0
* Author: Upskill HVAC
* Author URI: https://upskillhvac.com
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: hvac-community-events
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
// error_log('[HVAC DEBUG] Main plugin file hvac-community-events.php loaded.'); // REMOVED DEBUG LOG
// Define plugin constants
define('HVAC_CE_VERSION', '1.0.0');
define('HVAC_CE_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('HVAC_CE_PLUGIN_URL', plugin_dir_url(__FILE__));
/**
* Create required pages and roles upon plugin activation.
*/
function hvac_ce_create_required_pages() {
// Ensure the roles class is available
require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-roles.php';
error_log('HVAC CE: Activation hook fired.'); // Add logging start
$required_pages = [
'community-login' => [
'title' => 'Community Login',
'content' => '<!-- wp:shortcode -->[hvac_community_login]<!-- /wp:shortcode -->',
],
'trainer-registration' => [
'title' => 'Trainer Registration',
'content' => '<!-- wp:shortcode -->[hvac_trainer_registration]<!-- /wp:shortcode -->',
],
'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' => '<!-- wp:shortcode -->[tribe_community_events view="submission_form"]<!-- /wp:shortcode -->',
],
'my-events' => [ // New page for TEC CE event list shortcode
'title' => 'My Events',
'content' => '<!-- wp:shortcode -->[tribe_community_events view="my_events"]<!-- /wp:shortcode -->',
],
'trainer-profile' => [ // Add Trainer Profile page
'title' => 'Trainer Profile',
'content' => '<!-- wp:shortcode -->[hvac_trainer_profile]<!-- /wp:shortcode -->',
],
// 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 );

View file

@ -0,0 +1,152 @@
<?php
/**
* Main plugin class for HVAC Community Events
*/
if (!defined('ABSPATH')) {
exit;
}
class HVAC_Community_Events {
/**
* The single instance of the class
*/
private static $instance = null;
/**
* Main instance
*/
public static function instance() {
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
public function __construct() {
error_log('[HVAC DEBUG] HVAC_Community_Events constructor running.'); // ADDED LOG
$this->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

View file

@ -0,0 +1,312 @@
<?php
/**
* HVAC Community Events Dashboard Data Handler
*
* Retrieves and calculates data needed for the Trainer Dashboard.
*
* @package HVAC Community Events
* @subpackage Includes
* @author Roo
* @version 1.0.0
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class HVAC_Dashboard_Data
*
* Handles fetching and processing data for the trainer dashboard.
*/
class HVAC_Dashboard_Data {
/**
* The ID of the trainer user.
*
* @var int
*/
private int $user_id;
/**
* Constructor.
*
* @param int $user_id The ID of the trainer user.
*/
public function __construct( int $user_id ) {
$this->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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,93 @@
<?php
/**
* Handles custom roles and capabilities for the HVAC Community Events plugin
*/
if (!defined('ABSPATH')) {
exit;
}
class HVAC_Roles {
/**
* Create the hvac_trainer role with all required capabilities
*/
public function create_trainer_role() {
// Check if role already exists
if (get_role('hvac_trainer')) {
return;
}
// Add the role with capabilities
add_role(
'hvac_trainer',
__('HVAC Trainer', 'hvac-community-events'),
$this->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);
}
}

View file

@ -0,0 +1,71 @@
<?php
/**
* Handles plugin settings and options
*/
if (!defined('ABSPATH')) {
exit;
}
class HVAC_Settings {
public function __construct() {
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('admin_init', array($this, 'register_settings'));
}
public function add_admin_menu() {
add_options_page(
__('HVAC Community Events', 'hvac-ce'),
__('HVAC Community', 'hvac-ce'),
'manage_options',
'hvac-ce',
array($this, 'options_page')
);
}
public function register_settings() {
register_setting('hvac_ce_options', 'hvac_ce_options');
add_settings_section(
'hvac_ce_main',
__('HVAC Community Events Settings', 'hvac-ce'),
array($this, 'settings_section_callback'),
'hvac-ce'
);
add_settings_field(
'notification_emails',
__('Trainer Notification Emails', 'hvac-ce'),
array($this, 'notification_emails_callback'),
'hvac-ce',
'hvac_ce_main'
);
}
public function settings_section_callback() {
echo '<p>' . __('Configure settings for HVAC Community Events', 'hvac-ce') . '</p>';
}
public function notification_emails_callback() {
$options = get_option('hvac_ce_options');
echo '<input type="text" name="hvac_ce_options[notification_emails]" value="' .
esc_attr($options['notification_emails'] ?? '') . '" class="regular-text">';
echo '<p class="description">' .
__('Comma-separated list of emails to notify when new trainers register', 'hvac-ce') . '</p>';
}
public function options_page() {
?>
<div class="wrap">
<h1><?php esc_html_e('HVAC Community Events Settings', 'hvac-ce'); ?></h1>
<form method="post" action="options.php">
<?php
settings_fields('hvac_ce_options');
do_settings_sections('hvac-ce');
submit_button();
?>
</form>
</div>
<?php
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* Handles the display and processing of the event creation/modification form
* for HVAC Trainers. Leverages TEC Community Events functionality where possible.
*
* NOTE: This class is currently largely unused as functionality has been moved
* to using TEC Community Events shortcodes on dedicated pages. Kept for potential future use
* or if specific hooks are needed later.
*
* @package Hvac_Community_Events
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Class HVAC_Event_Handler
*/
class HVAC_Event_Handler {
/**
* Instance of this class.
* @var object
*/
protected static $instance = null;
/**
* Return an instance of this class.
* @return object A single instance of this class.
*/
public static function get_instance() {
// If the single instance hasn't been set, set it now.
if ( null === self::$instance ) {
self::$instance = new self();
self::$instance->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();

View file

@ -0,0 +1,227 @@
<?php
/**
* Handles data retrieval for the Event Summary page.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
class HVAC_Event_Summary_Data {
/**
* The ID of the event post.
*
* @var int|null
*/
private $event_id = null;
/**
* The event post object.
*
* @var WP_Post|null
*/
private $event_post = null;
/**
* Constructor.
*
* @param int $event_id The ID of the event to retrieve data for.
*/
public function __construct( $event_id ) {
$this->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;
}
}

View file

@ -0,0 +1,979 @@
<?php
/**
* Handles the HVAC trainer profile editing functionality.
*
* @package HVAC Community Events
* @subpackage Includes/Community
* @author Roo
* @version 1.0.0
*/
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly.
}
class HVAC_Profile {
const PROFILE_ACTION = 'hvac_update_profile'; // Action name for admin-post
const TRANSIENT_PREFIX = 'hvac_profile_'; // Prefix for transients
private static $instance = null; // Singleton instance
/**
* Get Singleton instance.
*
* @return HVAC_Profile
*/
public static function instance() {
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Private constructor to prevent direct instantiation.
*/
private function __construct() { // Make constructor private
// Register shortcode for profile form
add_shortcode('hvac_trainer_profile', array($this, 'render_profile_form'));
// Enqueue styles and scripts (reuse registration styles for now)
add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
// Hook to a later action and check for our specific POST action
add_action('wp_loaded', array($this, 'maybe_process_profile_submission'));
// <<< ADDED: Prevent canonical redirect from stripping our success param
add_filter('redirect_canonical', array($this, 'prevent_canonical_redirect_on_success'), 10, 2); // Restore filter
}
/**
* Prevents canonical redirects on the profile page if our success query var is present.
*
* @param string $redirect_url The redirect URL.
* @param string $requested_url The requested URL.
* @return string|false The redirect URL or false to prevent redirect.
*/
public function prevent_canonical_redirect_on_success($redirect_url, $requested_url) {
// Only interfere if it's the profile page and our success flag is set
// Use get_queried_object_id() to be more robust than is_page('slug')
$profile_page_id = get_page_by_path('trainer-profile', OBJECT, 'page') ? get_page_by_path('trainer-profile', OBJECT, 'page')->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 '<p>You must be logged in as a trainer to view this page.</p>';
}
$user_id = get_current_user_id();
$errors = [];
$success_message = '';
$submitted_data = [];
$output = ''; // Build output string
// Check for messages/errors from redirect
if (isset($_GET['profile_updated']) && $_GET['profile_updated'] === '1') {
$success_message = 'Profile updated successfully.';
$output .= '<!-- DEBUG_SUCCESS_FLAG_DETECTED -->'; // Keep debug marker
$output .= '<div class="hvac-notice hvac-success"><p>' . esc_html($success_message) . '</p></div>'; // Append to output
} elseif (isset($_GET['profile_error']) && $_GET['profile_error'] === '1' && isset($_GET['tid'])) {
$transient_id_from_url = sanitize_key($_GET['tid']);
$transient_key = self::TRANSIENT_PREFIX . $transient_id_from_url;
$transient_data = get_transient($transient_key);
if ($transient_data && is_array($transient_data)) {
$errors = $transient_data['errors'] ?? [];
$submitted_data = $transient_data['data'] ?? [];
delete_transient($transient_key);
} else {
$errors['transient'] = 'Could not retrieve submission details. Please try again.';
}
if (!empty($errors)) {
$output .= '<div class="hvac-errors">'; // Append to output
foreach ($errors as $error_key => $error_message) {
$output .= '<p class="error-message"><strong>Error:</strong> ' . esc_html($error_message) . '</p>'; // Append to output
}
$output .= '</div>'; // 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 '<p>Error: Could not load user data.</p>'; // 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: <a href="' . esc_url($venue_url) . '" target="_blank">' . esc_html($venue_title) . '</a>';
// 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 = '<div class="hvac-profile-form hvac-registration-form">'; // Reuse registration form class
$html .= '<h2>Edit Trainer Profile</h2>';
$html .= '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" id="hvac-trainer-profile-form" enctype="multipart/form-data" novalidate>';
$html .= '<input type="hidden" name="action" value="' . esc_attr(self::PROFILE_ACTION) . '">';
$html .= wp_nonce_field(self::PROFILE_ACTION, 'hvac_profile_nonce', true, false); // Return nonce field instead of echoing
// Account Information
$html .= '<div class="form-section">';
$html .= '<h3>Account Information</h3>';
$html .= '<div class="form-row">';
$html .= '<label for="user_email"><strong>Email *</strong></label>';
$html .= '<input type="email" name="user_email" id="user_email" value="' . esc_attr($form_data['user_email'] ?? '') . '" required aria-describedby="user_email_error">';
if (isset($errors['user_email'])) $html .= '<p class="error-message" id="user_email_error">' . esc_html($errors['user_email']) . '</p>';
$html .= '</div>';
$html .= '<div class="form-row form-row-half">';
$html .= '<div>';
$html .= '<label for="user_pass">New Password</label>';
$html .= '<input type="password" name="user_pass" id="user_pass" pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}" title="Password must be at least 8 characters long, and include at least one uppercase letter, one lowercase letter, and one number." aria-describedby="user_pass_hint user_pass_error">';
$html .= '<small id="user_pass_hint">Leave blank to keep current password. Must be 8+ chars, upper, lower, number.</small>';
if (isset($errors['user_pass'])) $html .= '<p class="error-message" id="user_pass_error">' . esc_html($errors['user_pass']) . '</p>';
$html .= '</div>';
$html .= '<div>';
$html .= '<label for="confirm_password">Confirm New Password</label>';
$html .= '<input type="password" name="confirm_password" id="confirm_password" aria-describedby="confirm_password_error">';
if (isset($errors['confirm_password'])) $html .= '<p class="error-message" id="confirm_password_error">' . esc_html($errors['confirm_password']) . '</p>';
$html .= '</div>';
$html .= '</div>';
$html .= '<div class="form-row">';
$html .= '<label for="current_password"><strong>Current Password *</strong></label>';
$html .= '<input type="password" name="current_password" id="current_password" required aria-describedby="current_password_hint current_password_error">';
$html .= '<small id="current_password_hint">Required to update email or password.</small>';
if (isset($errors['current_password'])) $html .= '<p class="error-message" id="current_password_error">' . esc_html($errors['current_password']) . '</p>';
$html .= '</div>';
$html .= '</div>'; // End Account Info
// Personal Information Section
$html .= '<div class="form-section">';
$html .= '<h3>Personal Information</h3>';
$html .= '<div class="form-row form-row-half">';
$html .= '<div>';
$html .= '<label for="first_name"><strong>First Name *</strong></label>';
$html .= '<input type="text" name="first_name" id="first_name" value="' . esc_attr($form_data['first_name'] ?? '') . '" required aria-describedby="first_name_error">';
if (isset($errors['first_name'])) $html .= '<p class="error-message" id="first_name_error">' . esc_html($errors['first_name']) . '</p>';
$html .= '</div>';
$html .= '<div>';
$html .= '<label for="last_name"><strong>Last Name *</strong></label>';
$html .= '<input type="text" name="last_name" id="last_name" value="' . esc_attr($form_data['last_name'] ?? '') . '" required aria-describedby="last_name_error">';
if (isset($errors['last_name'])) $html .= '<p class="error-message" id="last_name_error">' . esc_html($errors['last_name']) . '</p>';
$html .= '</div>';
$html .= '</div>';
$html .= '<div class="form-row">';
$html .= '<label for="display_name"><strong>Display Name *</strong></label>';
$html .= '<input type="text" name="display_name" id="display_name" value="' . esc_attr($form_data['display_name'] ?? '') . '" required aria-describedby="display_name_hint display_name_error">';
$html .= '<small id="display_name_hint">This will be the name displayed to other users on the site.</small>';
if (isset($errors['display_name'])) $html .= '<p class="error-message" id="display_name_error">' . esc_html($errors['display_name']) . '</p>';
$html .= '</div>';
$html .= '<div class="form-row form-row-half">';
$html .= '<div>';
$html .= '<label for="user_url">Personal Website (optional)</label>';
$html .= '<input type="url" name="user_url" id="user_url" value="' . esc_attr($form_data['user_url'] ?? '') . '" aria-describedby="user_url_error">';
if (isset($errors['user_url'])) $html .= '<p class="error-message" id="user_url_error">' . esc_html($errors['user_url']) . '</p>';
$html .= '</div>';
$html .= '<div>';
$html .= '<label for="user_linkedin">LinkedIn Profile URL (optional)</label>';
$html .= '<input type="url" name="user_linkedin" id="user_linkedin" value="' . esc_attr($form_data['user_linkedin'] ?? '') . '" aria-describedby="user_linkedin_error">';
if (isset($errors['user_linkedin'])) $html .= '<p class="error-message" id="user_linkedin_error">' . esc_html($errors['user_linkedin']) . '</p>';
$html .= '</div>';
$html .= '</div>';
$html .= '<div class="form-row">';
$html .= '<label for="personal_accreditation">Personal Accreditation (optional)</label>';
$html .= '<input type="text" name="personal_accreditation" id="personal_accreditation" value="' . esc_attr($form_data['personal_accreditation'] ?? '') . '" aria-describedby="personal_accreditation_hint">';
$html .= '<small id="personal_accreditation_hint">Enter your abbreviated accreditations separated by commas.</small>';
$html .= '</div>';
$html .= '<div class="form-row">';
$html .= '<label for="description"><strong>Biographical Info *</strong></label>';
$html .= '<textarea name="description" id="description" rows="4" required aria-describedby="description_hint description_error">' . esc_textarea($form_data['description'] ?? '') . '</textarea>';
$html .= '<small id="description_hint">A short bio about yourself. This will be displayed on your profile page.</small>';
if (isset($errors['description'])) $html .= '<p class="error-message" id="description_error">' . esc_html($errors['description']) . '</p>';
$html .= '</div>';
$html .= '<div class="form-row">';
$html .= '<label for="profile_image">Profile Image (optional)</label>';
if ($profile_image_url) {
$html .= '<div class="current-profile-image">';
$html .= '<img src="' . esc_url($profile_image_url) . '" alt="Current Profile Image" style="max-width: 100px; height: auto; margin-bottom: 10px;">';
$html .= '<label style="display: block; margin-bottom: 5px;">';
$html .= '<input type="checkbox" name="delete_profile_image" value="1"> Delete current image';
$html .= '</label>';
$html .= '</div>';
}
$html .= '<input type="file" name="profile_image" id="profile_image" accept="image/jpeg,image/png,image/gif" aria-describedby="profile_image_hint profile_image_error">';
$html .= '<small id="profile_image_hint">Upload a new image to replace the current one (.jpg, .png, .gif).</small>';
if (isset($errors['profile_image'])) $html .= '<p class="error-message" id="profile_image_error">' . esc_html($errors['profile_image']) . '</p>';
$html .= '</div>';
$html .= '</div>'; // End Personal Info
// Business Information Section
$html .= '<div class="form-section">';
$html .= '<h3>Business Information</h3>';
$html .= '<div class="form-row">';
$html .= '<label for="business_name"><strong>Business Name *</strong></label>';
$html .= '<input type="text" name="business_name" id="business_name" value="' . esc_attr($form_data['business_name'] ?? '') . '" required aria-describedby="business_name_error">';
if (isset($errors['business_name'])) $html .= '<p class="error-message" id="business_name_error">' . esc_html($errors['business_name']) . '</p>';
$html .= '</div>';
$html .= '<div class="form-row form-row-half">';
$html .= '<div>';
$html .= '<label for="business_phone"><strong>Business Phone *</strong></label>';
$html .= '<input type="tel" name="business_phone" id="business_phone" value="' . esc_attr($form_data['business_phone'] ?? '') . '" required aria-describedby="business_phone_error">';
if (isset($errors['business_phone'])) $html .= '<p class="error-message" id="business_phone_error">' . esc_html($errors['business_phone']) . '</p>';
$html .= '</div>';
$html .= '<div>';
$html .= '<label for="business_email"><strong>Business Email *</strong></label>';
$html .= '<input type="email" name="business_email" id="business_email" value="' . esc_attr($form_data['business_email'] ?? '') . '" required aria-describedby="business_email_error">';
if (isset($errors['business_email'])) $html .= '<p class="error-message" id="business_email_error">' . esc_html($errors['business_email']) . '</p>';
$html .= '</div>';
$html .= '</div>';
$html .= '<div class="form-row">';
$html .= '<label for="business_website">Business Website (optional)</label>';
$html .= '<input type="url" name="business_website" id="business_website" value="' . esc_attr($form_data['business_website'] ?? '') . '" aria-describedby="business_website_error">';
if (isset($errors['business_website'])) $html .= '<p class="error-message" id="business_website_error">' . esc_html($errors['business_website']) . '</p>';
$html .= '</div>';
$html .= '<div class="form-row">';
$html .= '<label for="business_description"><strong>Business Description *</strong></label>';
$html .= '<textarea name="business_description" id="business_description" rows="4" required aria-describedby="business_description_error">' . esc_textarea($form_data['business_description'] ?? '') . '</textarea>';
if (isset($errors['business_description'])) $html .= '<p class="error-message" id="business_description_error">' . esc_html($errors['business_description']) . '</p>';
$html .= '</div>';
$html .= '</div>'; // End Business Info
// Address Information Section
$html .= '<div class="form-section">';
$html .= '<h3>Address Information</h3>';
$html .= '<div class="form-row">';
$html .= '<label for="user_country"><strong>Country *</strong></label>';
$html .= '<select name="user_country" id="user_country" required aria-describedby="user_country_error">';
$html .= '<option value="">Select Country</option>';
$html .= '<option value="United States" ' . selected($form_data['user_country'] ?? '', 'United States', false) . '>United States</option>';
$html .= '<option value="Canada" ' . selected($form_data['user_country'] ?? '', 'Canada', false) . '>Canada</option>';
$html .= '<option value="" disabled>---</option>';
$countries = $this->get_country_list();
foreach ($countries as $code => $name) {
if ($code !== 'US' && $code !== 'CA') {
$html .= '<option value="' . esc_attr($name) . '" ' . selected($form_data['user_country'] ?? '', $name, false) . '>' . esc_html($name) . '</option>';
}
}
$html .= '</select>';
if (isset($errors['user_country'])) $html .= '<p class="error-message" id="user_country_error">' . esc_html($errors['user_country']) . '</p>';
$html .= '</div>';
$html .= '<div class="form-row form-row-half">';
$html .= '<div>';
$html .= '<label for="user_state"><strong>State/Province *</strong></label>';
$html .= '<select name="user_state" id="user_state" required aria-describedby="user_state_error">';
$html .= '<option value="">Select State/Province</option>';
$html .= '<option value="Other" ' . selected($form_data['user_state'] ?? '', 'Other', false) . '>Other</option>';
$selected_state = $form_data['user_state'] ?? ''; // Use form_data
if (!empty($selected_state) && $selected_state !== 'Other') {
$html .= '<option value="' . esc_attr($selected_state) . '" selected>' . esc_html($selected_state) . '</option>';
}
$html .= '</select>';
$other_style = (($form_data['user_state'] ?? '') === 'Other' && ($form_data['user_country'] ?? '') !== 'United States' && ($form_data['user_country'] ?? '') !== 'Canada') ? '' : 'display:none;';
$html .= '<input type="text" name="user_state_other" id="user_state_other" value="' . esc_attr($form_data['user_state_other'] ?? '') . '" style="' . $other_style . ' margin-top: 0.5rem;" placeholder="Enter your state/province" aria-describedby="user_state_other_error">';
if (isset($errors['user_state'])) $html .= '<p class="error-message" id="user_state_error">' . esc_html($errors['user_state']) . '</p>';
if (isset($errors['user_state_other'])) $html .= '<p class="error-message" id="user_state_other_error">' . esc_html($errors['user_state_other']) . '</p>';
$html .= '</div>';
$html .= '<div>';
$html .= '<label for="user_city"><strong>City *</strong></label>';
$html .= '<input type="text" name="user_city" id="user_city" value="' . esc_attr($form_data['user_city'] ?? '') . '" required aria-describedby="user_city_error">';
if (isset($errors['user_city'])) $html .= '<p class="error-message" id="user_city_error">' . esc_html($errors['user_city']) . '</p>';
$html .= '</div>';
$html .= '</div>';
$html .= '<div class="form-row">';
$html .= '<label for="user_zip"><strong>Zip/Postal Code *</strong></label>';
$html .= '<input type="text" name="user_zip" id="user_zip" value="' . esc_attr($form_data['user_zip'] ?? '') . '" required aria-describedby="user_zip_error">';
if (isset($errors['user_zip'])) $html .= '<p class="error-message" id="user_zip_error">' . esc_html($errors['user_zip']) . '</p>';
$html .= '</div>';
$html .= '</div>'; // End Address Info
// Training Venue Section
$html .= '<div class="form-section">';
$html .= '<h3>Training Venue</h3>';
$html .= '<div class="form-row">';
$html .= '<p>' . wp_kses_post($venue_info) . '</p>';
$html .= '</div>';
$html .= '</div>'; // End Training Venue
// Training Information Section
$html .= '<div class="form-section">';
$html .= '<h3>Training Information</h3>';
$html .= '<div class="form-row">';
$html .= '<label id="business_type_label"><strong>Business Type *</strong></label>';
$html .= '<small>What type of business are you?</small>';
$html .= '<div class="radio-group" role="radiogroup" aria-labelledby="business_type_label">';
$business_types = ["Manufacturer", "Distributor", "Contractor", "Consultant", "Educator", "Government", "Other"];
foreach ($business_types as $type) {
$html .= '<label><input type="radio" name="business_type" value="' . esc_attr($type) . '" ' . checked($form_data['business_type'] ?? '', $type, false) . ' required> ' . esc_html($type) . '</label>';
}
$html .= '</div>';
if (isset($errors['business_type'])) $html .= '<p class="error-message">' . esc_html($errors['business_type']) . '</p>';
$html .= '</div>';
$html .= '<div class="form-row">';
$html .= '<label id="training_audience_label"><strong>Training Audience *</strong></label>';
$html .= '<small>Who do you offer training to? (Select all that apply)</small>';
$html .= '<div class="checkbox-group" role="group" aria-labelledby="training_audience_label">';
$audience_options = [
"Anyone" => "Anyone (open to the public)",
"Industry professionals" => "Industry professionals",
"Internal staff" => "Internal staff in my company",
"Registered students" => "Registered students/members of my org/institution"
];
$selected_audience = $form_data['training_audience'] ?? []; // Use form_data
foreach ($audience_options as $value => $label) {
$html .= '<label><input type="checkbox" name="training_audience[]" value="' . esc_attr($value) . '" ' . checked(in_array($value, $selected_audience), true, false) . '> ' . esc_html($label) . '</label>';
}
$html .= '</div>';
if (isset($errors['training_audience'])) $html .= '<p class="error-message">' . esc_html($errors['training_audience']) . '</p>';
$html .= '</div>';
$html .= '<div class="form-row">';
$html .= '<label id="training_formats_label"><strong>Training Formats *</strong></label>';
$html .= '<small>What formats of training do you offer?</small>';
$html .= '<div class="checkbox-group" role="group" aria-labelledby="training_formats_label">';
$format_options = ["In-person", "Virtual", "Hybrid", "On-demand"];
$selected_formats = $form_data['training_formats'] ?? []; // Use form_data
foreach ($format_options as $option) {
$html .= '<label><input type="checkbox" name="training_formats[]" value="' . esc_attr($option) . '" ' . checked(in_array($option, $selected_formats), true, false) . '> ' . esc_html($option) . '</label>';
}
$html .= '</div>';
if (isset($errors['training_formats'])) $html .= '<p class="error-message">' . esc_html($errors['training_formats']) . '</p>';
$html .= '</div>';
$html .= '<div class="form-row">';
$html .= '<label id="training_locations_label"><strong>Training Locations *</strong></label>';
$html .= '<small>Where are you willing to provide training? (Select all that apply)</small>';
$html .= '<div class="checkbox-group" role="group" aria-labelledby="training_locations_label">';
$location_options = ["Online", "Local", "Regional", "Travel National", "Travel International"];
$selected_locations = $form_data['training_locations'] ?? []; // Use form_data
foreach ($location_options as $option) {
$html .= '<label><input type="checkbox" name="training_locations[]" value="' . esc_attr($option) . '" ' . checked(in_array($option, $selected_locations), true, false) . '> ' . esc_html($option) . '</label>';
}
$html .= '</div>';
if (isset($errors['training_locations'])) $html .= '<p class="error-message">' . esc_html($errors['training_locations']) . '</p>';
$html .= '</div>';
$html .= '<div class="form-row">';
$html .= '<label id="training_resources_label"><strong>Training Resources *</strong></label>';
$html .= '<small>What training resources do you have access to? (Select all that apply)</small>';
$html .= '<div class="checkbox-group" role="group" aria-labelledby="training_resources_label">';
$resource_options = [
"Classroom" => "Classroom",
"Training Lab" => "Training Lab",
"Ducted Furnace(s)" => "Ducted Furnace(s)",
"Ducted Air Handler(s)" => "Ducted Air Handler(s)",
"Ducted Air Conditioner(s)" => "Ducted Air Conditioner(s)",
"Ducted Heat Pump(s)" => "Ducted Heat Pump(s)",
"Ductless Heat Pump(s)" => "Ductless Heat Pump(s)",
"Training Manuals" => "Training Manuals",
"Presentation Slides" => "Presentation Slides",
"LMS Platform / SCORM Files" => "LMS Platform / SCORM Files",
"Custom Curriculum" => "Custom Curriculum",
"Other" => "Other"
];
$selected_resources = $form_data['training_resources'] ?? []; // Use form_data
foreach ($resource_options as $value => $label) {
$html .= '<label><input type="checkbox" name="training_resources[]" value="' . esc_attr($value) . '" ' . checked(in_array($value, $selected_resources), true, false) . '> ' . esc_html($label) . '</label>';
}
$html .= '</div>';
if (isset($errors['training_resources'])) $html .= '<p class="error-message">' . esc_html($errors['training_resources']) . '</p>';
$html .= '</div>';
$html .= '</div>'; // End Training Info
$html .= '<div class="form-row submit-row">';
$html .= '<input type="submit" value="Update Profile">';
$html .= '</div>';
$html .= '</form>';
$html .= '</div>'; // 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

View file

@ -0,0 +1,174 @@
<?php
/**
* Handles the Community Login page functionality.
*
* @package HVAC_Community_Events
* @version 1.0.0
*/
namespace HVAC_Community_Events\Community;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Login_Handler Class
*/
class Login_Handler {
/**
* Constructor.
* Hooks into WordPress.
*/
public function __construct() {
add_shortcode( 'hvac_community_login', array( $this, 'render_login_form' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); // Enqueue scripts/styles
// Add action hooks for authentication and redirection (Task 2.2 & 2.5)
add_action( 'wp_authenticate', array( $this, 'handle_authentication' ), 30, 2 ); // Allow custom auth checks
// REMOVED: add_action( 'login_form_login', array( $this, 'redirect_on_login_failure' ) ); // This was causing premature redirects
add_action( 'wp_login_failed', array( $this, 'handle_login_failure' ) ); // Handle failed login redirect
add_filter( 'login_redirect', array( $this, 'custom_login_redirect' ), 10, 3 ); // Handle success redirect
// Redirect logged-in users away from the login page
add_action( 'template_redirect', array( $this, 'redirect_logged_in_user' ) );
}
/**
* Renders the login form using the custom template.
*
* @param array $atts Shortcode attributes.
* @return string HTML output of the login form.
*/
public function render_login_form( $atts ) {
// Logged-in user check and redirect moved to redirect_logged_in_user() hooked to template_redirect
// Start output buffering to capture the template output.
ob_start();
// Check for login errors passed via query parameters
if ( isset( $_GET['login'] ) && $_GET['login'] === 'failed' ) {
// You might want to use a more user-friendly message or integrate with theme notices
echo '<div class="hvac-login-error" style="color: red; border: 1px solid red; padding: 10px; margin-bottom: 15px;">' . esc_html__( 'Invalid username or password.', 'hvac-community-events' ) . '</div>';
}
// 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 '<p>Error: Login form template not found.</p>';
}
// 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;
}
}
}

View file

@ -0,0 +1,67 @@
<?php
/**
* HVAC Community Events: Custom Login Form Template
*
* This template provides the structure for the custom login page,
* integrating with the Astra theme's styling.
*
* @version 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// Get Astra theme layout settings if needed, e.g., container width
// $container_class = astra_get_content_layout(); // Example
?>
<div class="hvac-community-login-wrapper"> <?php // Custom wrapper for potential styling ?>
<div class="ast-container"> <?php // Astra theme container ?>
<div class="hvac-login-form-card"> <?php // Card styling based on design reference ?>
<?php
// Display login errors if any
// Example: Check for login errors passed via query parameters or session
// if ( isset( $_GET['login'] ) && $_GET['login'] === 'failed' ) {
// echo '<p class="login-error">Invalid username or password.</p>';
// }
?>
<?php
// Arguments for wp_login_form
$args = array(
'echo' => 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 );
?>
<div class="hvac-login-links">
<?php if ( get_option( 'users_can_register' ) ) : ?>
<a class="hvac-register-link" href="<?php echo esc_url( wp_registration_url() ); ?>">
<?php esc_html_e( 'Register', 'hvac-community-events' ); ?>
</a> |
<?php endif; ?>
<a class="hvac-lostpassword-link" href="<?php echo esc_url( wp_lostpassword_url() ); ?>">
<?php esc_html_e( 'Lost your password?', 'hvac-community-events' ); // Task 2.4 ?>
</a>
</div>
</div> <?php // .hvac-login-form-card ?>
</div> <?php // .ast-container ?>
</div> <?php // .hvac-community-login-wrapper ?>

View file

@ -0,0 +1,222 @@
<?php
/**
* Template for displaying single HVAC Event Summary.
*
* This template overrides the default single event template provided by The Events Calendar
* when viewed by users with appropriate permissions (or potentially all users, depending on requirements).
* It leverages the Astra theme structure where possible.
*
* Design Reference: design_references/upskillhvac.com_hce-event-summary__event_id=1662 (1).png
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// Ensure the data class is available
if ( ! class_exists( 'HVAC_Event_Summary_Data' ) ) {
// Attempt to include it if not loaded - adjust path as needed
$class_path = plugin_dir_path( __FILE__ ) . '../includes/community/class-event-summary-data.php';
if ( file_exists( $class_path ) ) {
require_once $class_path;
} else {
// Handle error: Class not found, cannot display summary
echo "<p>Error: Event Summary data handler not found.</p>";
return;
}
}
get_header();
?>
<div id="primary" <?php astra_primary_class(); ?>>
<?php astra_primary_content_top(); ?>
<?php astra_content_loop(); // This typically includes the have_posts() and the_post() loop ?>
<?php
// Ensure we are inside the loop and it's the correct post type
if ( have_posts() && get_post_type() === Tribe__Events__Main::POSTTYPE ) {
the_post();
$event_id = get_the_ID();
$summary_data_handler = new HVAC_Event_Summary_Data( $event_id );
if ( $summary_data_handler->is_valid_event() ) {
$event_details = $summary_data_handler->get_event_details();
$venue_details = $summary_data_handler->get_event_venue_details();
$organizer_details = $summary_data_handler->get_event_organizer_details();
$transactions = $summary_data_handler->get_event_transactions();
?>
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<header class="entry-header <?php astra_entry_header_class(); ?>">
<!-- Task 5.3: Implement breadcrumb navigation using theme's breadcrumb component -->
<?php
// Check if Astra breadcrumb function exists and call it
if ( function_exists( 'astra_get_breadcrumb' ) ) {
astra_get_breadcrumb();
} else {
// Fallback or alternative breadcrumb can be added here if needed
echo '<!-- Breadcrumb not available -->';
}
?>
<?php the_title( '<h1 class="entry-title">', '</h1>' ); ?>
<!-- Add Edit Event Button (Task 5.6) - Conditionally shown for trainer -->
<?php
// Check if the current user can edit this specific event post
// Using 'edit_post' capability for now, might need refinement to a custom cap later
if ( current_user_can( 'edit_post', $event_id ) ) {
// Get the URL of the 'manage-event' page
$manage_event_page = get_page_by_path( 'manage-event' );
if ( $manage_event_page ) {
$edit_url = get_permalink( $manage_event_page->ID );
$edit_url = add_query_arg( 'event_id', $event_id, $edit_url );
// Apply Astra button classes
printf(
'<a href="%s" class="button astra-button">%s</a>',
esc_url( $edit_url ),
esc_html__( 'Edit Event', 'hvac-community-events' )
);
}
}
?>
<!-- View Public Page Button -->
<a href="<?php echo esc_url( get_permalink($event_id) ); ?>" class="button astra-button" target="_blank" style="margin-left: 1em;">View Public Event Page</a>
<!-- Email Attendees Button (Phase 2) -->
<?php
// TODO: Add capability check for emailing attendees (e.g., 'email_hvac_attendees')
$can_email = current_user_can( 'edit_post', $event_id ); // Placeholder: Use edit cap for now
if ( $can_email ) {
// TODO: Link to actual Email Attendees page (Phase 2)
$email_attendees_url = '#'; // Placeholder URL
printf(
'<a href="%s" class="button astra-button" style="margin-left: 1em;">%s</a>',
esc_url( $email_attendees_url ),
esc_html__( 'Email Attendees', 'hvac-community-events' )
);
}
?>
</header> <!-- .entry-header -->
<div class="entry-content clear" <?php astra_schema_e( 'text' ); ?>>
<?php astra_entry_content_before(); ?>
<!-- Task 5.2 & 5.4: Display Event Details in theme-styled card sections / Format content -->
<div class="hvac-event-summary-details">
<h2>Event Details</h2>
<?php if ( $event_details ) { ?>
<p><strong>Date:</strong> <?php
if ( function_exists( 'tribe_events_event_schedule_details' ) ) {
echo tribe_events_event_schedule_details( $event_id );
} else {
// Fallback display if function doesn't exist
echo esc_html( $event_details['start_date'] ?? 'N/A' ) . ' - ' . esc_html( $event_details['end_date'] ?? 'N/A' );
}
?></p>
<p><strong>Cost:</strong> <?php echo esc_html( $event_details['cost'] ?? 'N/A' ); ?></p>
<div class="event-description">
<?php echo wp_kses_post( $event_details['description'] ); ?>
</div>
<?php } ?>
<?php if ( $venue_details ) { ?>
<h3>Venue</h3>
<p><strong>Name:</strong> <?php echo esc_html( $venue_details['name'] ); ?></p>
<?php if ( ! empty( $venue_details['address'] ) ) : ?>
<p><strong>Address:</strong> <?php echo esc_html( $venue_details['address'] ); ?></p>
<?php endif; ?>
<?php if ( ! empty( $venue_details['phone'] ) ) : ?>
<p><strong>Phone:</strong> <?php echo esc_html( $venue_details['phone'] ); ?></p>
<?php endif; ?>
<?php if ( ! empty( $venue_details['website'] ) ) : ?>
<p><strong>Website:</strong> <?php echo wp_kses_post( $venue_details['website'] ); // Allow link HTML ?></p>
<?php endif; ?>
<?php // TODO: Add Map Link / Directions Link if needed ?>
<?php } ?>
<?php if ( $organizer_details ) { ?>
<h3>Organizer</h3>
<p><strong>Name:</strong> <?php echo esc_html( $organizer_details['name'] ); ?></p>
<?php if ( ! empty( $organizer_details['phone'] ) ) : ?>
<p><strong>Phone:</strong> <?php echo esc_html( $organizer_details['phone'] ); ?></p>
<?php endif; ?>
<?php if ( ! empty( $organizer_details['email'] ) ) : ?>
<p><strong>Email:</strong> <a href="mailto:<?php echo esc_attr( $organizer_details['email'] ); ?>"><?php echo esc_html( $organizer_details['email'] ); ?></a></p>
<?php endif; ?>
<?php if ( ! empty( $organizer_details['website'] ) ) : ?>
<p><strong>Website:</strong> <?php echo wp_kses_post( $organizer_details['website'] ); // Allow link HTML ?></p>
<?php endif; ?>
<?php } ?>
</div>
<!-- Task 5.5: Implement Transactions Table using theme's table styling -->
<div class="hvac-event-summary-transactions">
<h2>Transactions / Attendees</h2>
<?php if ( ! empty( $transactions ) ) { ?>
<table class="hvac-transactions-table astra-table-cls"> <!-- Add theme table class -->
<thead>
<tr>
<th>Attendee Name</th>
<th>Email</th>
<th>Ticket Type</th>
<th>Order ID</th>
<th>Checked In</th>
</tr>
</thead>
<tbody>
<?php foreach ( $transactions as $txn ) { ?>
<tr>
<td><?php echo esc_html( $txn['purchaser_name'] ?? 'N/A' ); ?></td>
<td><?php echo esc_html( $txn['purchaser_email'] ?? 'N/A' ); ?></td>
<td><?php echo esc_html( $txn['ticket_type_name'] ?? 'N/A' ); ?></td>
<td><?php echo esc_html( $txn['order_id'] ?? 'N/A' ); ?></td>
<td><?php echo $txn['checked_in'] ? 'Yes' : 'No'; ?></td>
</tr>
<?php } ?>
</tbody>
</table>
<?php } else { ?>
<p>No transactions found for this event.</p>
<?php } ?>
</div>
<?php wp_link_pages( /* ... */ ); ?>
<?php astra_entry_content_after(); ?>
</div><!-- .entry-content -->
</article><!-- #post-<?php the_ID(); ?> -->
<?php
} else {
// Handle case where event data couldn't be loaded
echo '<p>Could not load event summary data.</p>';
}
} 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 '<p>Event not found or invalid post type.</p>';
}
?>
<?php astra_primary_content_bottom(); ?>
</div><!-- #primary -->
<?php get_sidebar(); ?>
<?php get_footer(); ?>

View file

@ -0,0 +1,220 @@
<?php
/**
* Template Name: HVAC Trainer Dashboard
*
* This template handles the display of the HVAC Trainer Dashboard.
* It checks for user login and role, then displays stats and events.
*
* @package HVAC Community Events
* @subpackage Templates
* @author Roo
* @version 1.0.1
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// --- Security Check &amp; Data Loading ---
// Ensure user is logged in and has the correct role
if ( ! is_user_logged_in() || ! current_user_can( 'view_hvac_dashboard' ) ) {
// Redirect to login page or show an error message
wp_safe_redirect( home_url( '/community-login/' ) ); // Redirect to the custom login page
exit;
}
// Get the current user ID
$user_id = get_current_user_id();
// Include and instantiate the dashboard data class
require_once HVAC_CE_PLUGIN_DIR . 'includes/class-hvac-dashboard-data.php';
$dashboard_data = new HVAC_Dashboard_Data( $user_id );
// Fetch data
$total_events = $dashboard_data->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
?>
<div id="primary" class="content-area primary ast-container"> <!-- Use Astra container -->
<main id="main" class="site-main">
<!-- Dashboard Header &amp; Navigation -->
<div class="hvac-dashboard-header ast-flex ast-justify-content-space-between ast-align-items-center">
<h1 class="entry-title">Trainer Dashboard</h1> <!-- Standard WP title class -->
<div class="hvac-dashboard-nav">
<?php // TODO: Add icons to buttons via CSS ?>
<a href="<?php echo esc_url( home_url( '/trainer-profile/' ) ); ?>" class="ast-button ast-button-primary">Edit Profile</a> <?php // TODO: Implement trainer profile page ?>
<a href="<?php echo esc_url( home_url( '/manage-event/' ) ); ?>" class="ast-button ast-button-primary">Create Event</a>
<a href="<?php echo esc_url( wp_logout_url( home_url( '/community-login/' ) ) ); ?>" class="ast-button ast-button-primary">Logout</a>
</div>
</div>
<!-- Statistics Section -->
<section class="hvac-dashboard-stats">
<h2 class="section-title">Your Stats</h2>
<div class="ast-row hvac-stats-grid"> <!-- Use Astra grid row - Add custom class for 5 columns -->
<!-- Stat Card: Total Events -->
<div class="ast-col"> <!-- Use default col for flex/custom grid -->
<div class="hvac-stat-card">
<?php // TODO: Add icon via CSS ?>
<span class="stat-value"><?php echo esc_html( $total_events ); ?></span>
<span class="stat-label">Total Events</span>
</div>
</div>
<!-- Stat Card: Upcoming Events -->
<div class="ast-col">
<div class="hvac-stat-card">
<?php // TODO: Add icon via CSS ?>
<span class="stat-value"><?php echo esc_html( $upcoming_events ); ?></span>
<span class="stat-label">Upcoming Events</span>
</div>
</div>
<!-- Stat Card: Past Events -->
<div class="ast-col">
<div class="hvac-stat-card">
<?php // TODO: Add icon via CSS ?>
<span class="stat-value"><?php echo esc_html( $past_events ); ?></span>
<span class="stat-label">Past Events</span>
</div>
</div>
<!-- Stat Card: Total Tickets Sold -->
<div class="ast-col">
<div class="hvac-stat-card">
<?php // TODO: Add icon via CSS ?>
<span class="stat-value"><?php echo esc_html( $total_sold ); ?></span>
<span class="stat-label">Tickets Sold</span>
</div>
</div>
<!-- Stat Card: Total Revenue -->
<div class="ast-col">
<div class="hvac-stat-card">
<?php // TODO: Add icon via CSS ?>
<span class="stat-value">$<?php echo esc_html( number_format( $total_revenue, 2 ) ); ?></span>
<span class="stat-label">Total Revenue</span>
<?php // Note: Target revenue not shown in desired design ?>
</div>
</div>
</div> <!-- /.ast-row -->
</section>
<!-- Events Table Section -->
<section class="hvac-dashboard-events">
<h2 class="section-title">Your Events</h2>
<!-- Tab Filters -->
<?php
// --- Event Filters (Tabs) ---
$dashboard_url = get_permalink(); // Get the current page URL
$current_filter = isset( $_GET['event_time'] ) ? sanitize_key( $_GET['event_time'] ) : 'all'; // Change query param
$filter_tabs = [ // Change filter options
'all' => 'All Events',
'upcoming' => 'Upcoming',
'past' => 'Past'
];
?>
<div class="hvac-event-tabs">
<ul class="hvac-tabs-nav">
<?php foreach ($filter_tabs as $key => $label) :
$url = add_query_arg( 'event_time', $key, $dashboard_url );
$class = 'hvac-tab-link';
if ($key === $current_filter) {
$class .= ' active'; // Add active class
}
// Special case for 'all' filter link
if ($key === 'all') {
$url = remove_query_arg( 'event_time', $dashboard_url );
}
?>
<li><a href="<?php echo esc_url( $url ); ?>" class="<?php echo esc_attr( $class ); ?>"><?php echo esc_html( $label ); ?></a></li>
<?php endforeach; ?>
</ul>
</div>
<!-- Events Table -->
<?php
// Fetch events based on the new time filter ('all', 'upcoming', 'past')
// Note: get_events_table_data likely needs modification to handle time filtering instead of status
$events = $dashboard_data->get_events_table_data( $current_filter ); // TODO: Update HVAC_Dashboard_Data::get_events_table_data to accept 'all', 'upcoming', 'past'
?>
<div class="hvac-events-table-wrapper">
<table class="wp-list-table widefat fixed striped events-table hvac-events-table"> <!-- Add custom class for styling -->
<thead>
<tr>
<th scope="col" class="manage-column column-status">Status</th>
<th scope="col" class="manage-column column-title">Event Name</th>
<th scope="col" class="manage-column column-date">Date</th>
<th scope="col" class="manage-column column-organizer">Organizer</th>
<th scope="col" class="manage-column column-capacity">Capacity</th>
<th scope="col" class="manage-column column-sold">Sold</th>
<th scope="col" class="manage-column column-revenue">Revenue</th>
<th scope="col" class="manage-column column-actions">Actions</th> <!-- Added Actions Column -->
</tr>
</thead>
<tbody id="the-list">
<?php if ( ! empty( $events ) ) : ?>
<?php foreach ( $events as $event ) : ?>
<tr>
<td class="column-status"><?php echo esc_html( ucfirst( $event['status'] ) ); ?></td>
<td class="column-title">
<strong><a href="<?php echo esc_url( $event['link'] ); ?>" target="_blank"><?php echo esc_html( $event['name'] ); ?></a></strong>
<!-- Add Edit/View links below title if needed -->
</td>
<td class="column-date"><?php echo esc_html( date( 'Y-m-d H:i', $event['start_date_ts'] ) ); ?></td>
<td class="column-organizer"><?php
// Check if tribe_get_organizer function exists before calling
if ( function_exists( 'tribe_get_organizer' ) ) {
echo esc_html( tribe_get_organizer( $event['organizer_id'] ) );
} else {
echo 'Organizer ID: ' . esc_html( $event['organizer_id'] ); // Fallback
}
?></td>
<td class="column-capacity"><?php echo esc_html( $event['capacity'] ); ?></td>
<td class="column-sold"><?php echo esc_html( $event['sold'] ); ?></td>
<td class="column-revenue">$<?php echo esc_html( number_format( $event['revenue'], 2 ) ); ?></td>
<td class="column-actions">
<?php
// Link to the page containing the TEC CE submission form shortcode
$edit_url = add_query_arg( 'event_id', $event['id'], home_url( '/manage-event/' ) );
// Link to the custom event summary page
$summary_url = get_permalink( $event['id'] ); // Assumes custom template is loaded for event post type
?>
<?php // TODO: Add icons via CSS ?>
<a href="<?php echo esc_url( $edit_url ); ?>" class="ast-button ast-button-primary button-small">Edit</a>
<a href="<?php echo esc_url( $summary_url ); ?>" class="ast-button ast-button-primary button-small">View Summary</a>
</td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr>
<td colspan="8">No events found.</td> <!-- Updated colspan -->
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</main> <!-- #main -->
</div> <!-- #primary -->
<?php
get_footer(); // Use theme's footer
?>