Compare commits
3 commits
8adc3ac8e4
...
cb68c9a5bf
| Author | SHA1 | Date | |
|---|---|---|---|
| cb68c9a5bf | |||
| ca928bfffb | |||
| 25bf5d98e1 |
16 changed files with 1541 additions and 83 deletions
|
|
@ -19,7 +19,67 @@
|
||||||
"Bash(curl:*)",
|
"Bash(curl:*)",
|
||||||
"Bash(yes:*)",
|
"Bash(yes:*)",
|
||||||
"WebFetch(domain:json.schemastore.org)",
|
"WebFetch(domain:json.schemastore.org)",
|
||||||
"WebFetch(domain:www.schemastore.org)"
|
"WebFetch(domain:www.schemastore.org)",
|
||||||
|
"Bash(scripts/pre-deployment-check.sh)",
|
||||||
|
"Bash(ssh roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp eval ''wp_set_password\\(\"\"TestPass123\"\", 25\\);'' 2>&1\")",
|
||||||
|
"Bash(wc:*)",
|
||||||
|
"Bash(php -l:*)",
|
||||||
|
"Bash(php:*)",
|
||||||
|
"Bash(./scripts/pre-deployment-check.sh:*)",
|
||||||
|
"Bash(HEADLESS=true BASE_URL=https://upskill-staging.measurequick.com node test-comprehensive-validation.js:*)",
|
||||||
|
"Bash(ssh roodev@146.190.76.204 'cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp user get test_master --field=ID 2>&1')",
|
||||||
|
"Bash(ssh:*)",
|
||||||
|
"mcp__playwright__browser_wait_for",
|
||||||
|
"mcp__playwright__browser_console_messages",
|
||||||
|
"mcp__playwright__browser_snapshot",
|
||||||
|
"mcp__playwright__browser_navigate_back",
|
||||||
|
"mcp__playwright__browser_press_key",
|
||||||
|
"mcp__playwright__browser_network_requests",
|
||||||
|
"Bash(source:*)",
|
||||||
|
"Bash(sshpass -p \"$UPSKILL_STAGING_PASS\" ssh -o StrictHostKeyChecking=no \"$UPSKILL_STAGING_SSH_USER@$UPSKILL_STAGING_IP\" \"cd $UPSKILL_STAGING_PATH && wp eval ''HVAC_Page_Manager::create_pages\\(\\); echo \"\"Pages created/verified\"\";'' 2>&1\")",
|
||||||
|
"mcp__playwright__browser_select_option",
|
||||||
|
"mcp__playwright__browser_hover",
|
||||||
|
"mcp__playwright__browser_resize",
|
||||||
|
"Bash(grep:*)",
|
||||||
|
"Bash(sshpass -p \"$UPSKILL_STAGING_PASS\" ssh -o StrictHostKeyChecking=no \"roodev@146.190.76.204\" \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp eval \"\"HVAC_Secure_Storage::store_credential\\(''hvac_google_maps_api_key'', ''AIzaSyD_W0fPmA3wtsQ_o3mdeoaUpxX_cdBvi5g''\\); echo ''API key stored successfully'';\"\" && wp cache flush\")",
|
||||||
|
"Bash(sshpass -p \"$UPSKILL_PROD_SSH_PASS\" ssh -o StrictHostKeyChecking=no \"$UPSKILL_PROD_SSH_USER@$UPSKILL_PROD_IP\" \"cd $UPSKILL_PROD_PATH && wp eval \"\"HVAC_Secure_Storage::save_credential\\(''hvac_google_maps_api_key'', ''AIzaSyD_W0fPmA3wtsQ_o3mdeoaUpxX_cdBvi5g''\\); echo ''API key saved successfully'';\"\"\")",
|
||||||
|
"Bash(sshpass -p \"$UPSKILL_PROD_SSH_PASS\" ssh -o StrictHostKeyChecking=no \"$UPSKILL_PROD_SSH_USER@$UPSKILL_PROD_IP\" \"cd $UPSKILL_PROD_PATH && wp eval \"\"HVAC_Secure_Storage::store_credential\\(''hvac_google_maps_api_key'', ''AIzaSyD_W0fPmA3wtsQ_o3mdeoaUpxX_cdBvi5g''\\); echo ''API key saved successfully'';\"\"\")",
|
||||||
|
"Bash(sshpass -p \"$UPSKILL_PROD_SSH_PASS\" ssh -o StrictHostKeyChecking=no \"$UPSKILL_PROD_SSH_USER@$UPSKILL_PROD_IP\" \"cd $UPSKILL_PROD_PATH && wp cache flush\")",
|
||||||
|
"Bash(sshpass -f ~/.ssh/cloudways_pass rsync:*)",
|
||||||
|
"Bash(rsync:*)",
|
||||||
|
"Bash(if [ -f /home/ben/dev/upskill-event-manager/.env ])",
|
||||||
|
"Bash(then echo \".env exists\")",
|
||||||
|
"Bash(else echo \".env not found\")",
|
||||||
|
"Bash(fi)",
|
||||||
|
"mcp__wordpress-production__wp_cpt_search",
|
||||||
|
"mcp__wordpress-production__wp_get_cpt",
|
||||||
|
"mcp__wordpress-production__wp_update_cpt",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"mcp__wordpress-production__wp_users_search",
|
||||||
|
"mcp__wordpress-production__wp_add_user",
|
||||||
|
"Bash(node --check:*)",
|
||||||
|
"mcp__playwright__browser_tab_list",
|
||||||
|
"Bash(scp:*)",
|
||||||
|
"Bash(sshpass -p 'uSCO6f1y' scp -o StrictHostKeyChecking=no scripts/setup-approved-labs.php scripts/check-venue-coordinates.php scripts/geocode-approved-labs.php roodev@146.190.76.204:/home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/plugins/hvac-community-events/scripts/)",
|
||||||
|
"Bash(sshpass -p 'uSCO6f1y' ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"ls -la /home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/plugins/hvac-community-events/includes/class-hvac-venue-categories.php 2>/dev/null || echo ''File not found''\")",
|
||||||
|
"Bash(sshpass -p 'uSCO6f1y' ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"grep -n ''venue-categories\\\\|Venue_Categories'' /home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/plugins/hvac-community-events/includes/class-hvac-plugin.php\")",
|
||||||
|
"Bash(sshpass -p 'uSCO6f1y' ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp eval ''if \\(class_exists\\(\"\"HVAC_Venue_Categories\"\"\\)\\) { echo \"\"Class exists\\\\n\"\"; HVAC_Venue_Categories::instance\\(\\)->register_taxonomies\\(\\); echo \"\"Registered\\\\n\"\"; } else { echo \"\"Class not found\\\\n\"\"; } echo \"\"venue_type exists: \"\" . \\(taxonomy_exists\\(\"\"venue_type\"\"\\) ? \"\"yes\"\" : \"\"no\"\"\\) . \"\"\\\\n\"\";''\")",
|
||||||
|
"Bash(sshpass -p 'uSCO6f1y' scp -o StrictHostKeyChecking=no scripts/check-venue-coordinates.php roodev@146.190.76.204:/home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/plugins/hvac-community-events/scripts/)",
|
||||||
|
"Bash(sshpass -p 'uSCO6f1y' ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp eval-file wp-content/plugins/hvac-community-events/scripts/check-venue-coordinates.php\")",
|
||||||
|
"Bash(sshpass -p 'uSCO6f1y' ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp eval ''\n// Simulate AJAX context - just check what happens without forcing registration\necho \"\"TEC active: \"\" . \\(function_exists\\(\"\"tribe_get_venue\"\"\\) ? \"\"yes\"\" : \"\"no\"\"\\) . \"\"\\\\n\"\";\necho \"\"venue_type taxonomy: \"\" . \\(taxonomy_exists\\(\"\"venue_type\"\"\\) ? \"\"yes\"\" : \"\"no\"\"\\) . \"\"\\\\n\"\";\n\n// Now test the actual method\nif \\(class_exists\\(\"\"HVAC_Training_Map_Data\"\"\\)\\) {\n $data = HVAC_Training_Map_Data::get_instance\\(\\);\n $venues = $data->get_venue_markers\\(\\);\n echo \"\"get_venue_markers\\(\\) returned: \"\" . count\\($venues\\) . \"\" venues\\\\n\"\";\n}\n''\")",
|
||||||
|
"Bash(sshpass -p 'uSCO6f1y' scp:*)",
|
||||||
|
"Bash(sshpass -p 'uSCO6f1y' ssh -o StrictHostKeyChecking=no roodev@146.190.76.204 \"cd /home/974670.cloudwaysapps.com/uberrxmprk/public_html && wp eval ''if \\(class_exists\\(\"\"HVAC_Training_Map_Data\"\"\\)\\) { $d = HVAC_Training_Map_Data::get_instance\\(\\); $v = $d->get_venue_markers\\(\\); foreach \\($v as $x\\) { echo $x[\"\"name\"\"] . \"\"\\\\n\"\"; } }''\")",
|
||||||
|
"Bash(sshpass:*)",
|
||||||
|
"WebFetch(domain:upskillhvac.com)",
|
||||||
|
"Bash(pkill:*)",
|
||||||
|
"Bash(echo:*)",
|
||||||
|
"WebFetch(domain:upskill-staging.measurequick.com)",
|
||||||
|
"Bash(bash:*)",
|
||||||
|
"mcp__playwright__browser_handle_dialog",
|
||||||
|
"Bash(/home/ben/dev/upskill-event-manager/scripts/pre-deployment-check.sh:*)",
|
||||||
|
"Bash(/home/ben/dev/upskill-event-manager/scripts/deploy.sh:*)",
|
||||||
|
"Bash(printf:*)",
|
||||||
|
"WebFetch(domain:issuetracker.google.com)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": [],
|
"ask": [],
|
||||||
|
|
|
||||||
50
CLAUDE.md
50
CLAUDE.md
|
|
@ -180,3 +180,53 @@ The plugin still relies on "The Events Calendar: Community" (TEC CE) for event c
|
||||||
- **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)** - System architecture details
|
- **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)** - System architecture details
|
||||||
- **[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)** - Common issues and solutions
|
- **[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)** - Common issues and solutions
|
||||||
- **[docs/TESTING-GUIDE.md](docs/TESTING-GUIDE.md)** - Testing procedures
|
- **[docs/TESTING-GUIDE.md](docs/TESTING-GUIDE.md)** - Testing procedures
|
||||||
|
|
||||||
|
## AI Assistant Tools
|
||||||
|
|
||||||
|
### Zen MCP Tools (Use Throughout Every Session)
|
||||||
|
- **Zen Debug**: Use whenever you encounter an error or unexpected behavior
|
||||||
|
- **Zen Code Review**: Use whenever you finish a significant piece of code or feature
|
||||||
|
- **Zen Analyze**: Use before adding new features or making significant changes to existing code
|
||||||
|
- **Zen Deepthink**: Use when you have a complex problem to solve or need to brainstorm ideas
|
||||||
|
- **Zen Chat**: Use for general questions, clarifications, or when you need assistance with a task
|
||||||
|
|
||||||
|
**Important Context Reminder**: Remind the Zen tools that this is a small local environment with a single user, so security and scalability are not primary concerns.
|
||||||
|
|
||||||
|
### Clink Multi-Model Workflows (GPT-5, Gemini 3, Kimi K2.5)
|
||||||
|
|
||||||
|
Use `mcp__zen__clink` to get diverse AI perspectives via external CLIs. Available CLIs:
|
||||||
|
- **codex**: GPT-5 - Fast, precise code understanding
|
||||||
|
- **gemini**: Gemini 3 - Large context (1M tokens), thorough analysis
|
||||||
|
- **Kimi K2.5** (via `mcp__zen__chat` with OpenRouter) - Strong reasoning, excellent at complex logic
|
||||||
|
|
||||||
|
**Security Warning:** Files are sent to OpenAI, Google, and Moonshot AI. NEVER use on `.env`, credentials, or regulated data.
|
||||||
|
|
||||||
|
**When to Use Multi-Model:**
|
||||||
|
- Code reviews (catch different types of issues)
|
||||||
|
- Complex debugging (multiple hypotheses)
|
||||||
|
- Architecture decisions (diverse perspectives)
|
||||||
|
- Planning (compare approaches)
|
||||||
|
|
||||||
|
**Available Skills:**
|
||||||
|
- `/multi-review <files>` - Code review from all 3 models
|
||||||
|
- `/multi-plan <task>` - Planning perspectives from all 3 models
|
||||||
|
- `/multi-debug <issue>` - Parallel bug investigation
|
||||||
|
- `/multi-analyze <files>` - Comprehensive analysis
|
||||||
|
|
||||||
|
**Quick Usage** (all three calls in one message = parallel execution):
|
||||||
|
```python
|
||||||
|
review_prompt = "Review for bugs and security issues"
|
||||||
|
mcp__zen__clink(prompt=review_prompt, cli_name="codex", role="codereviewer", absolute_file_paths=["/absolute/path/file.py"])
|
||||||
|
mcp__zen__clink(prompt=review_prompt, cli_name="gemini", role="codereviewer", absolute_file_paths=["/absolute/path/file.py"])
|
||||||
|
mcp__zen__chat(prompt=review_prompt + " Use Kimi K2.5 via OpenRouter for this analysis.", absolute_file_paths=["/absolute/path/file.py"])
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** Use absolute paths (not globs). Resolve patterns first: `find src -name "*.py" | xargs realpath`
|
||||||
|
|
||||||
|
**Synthesis Format:** After multi-model analysis, report:
|
||||||
|
1. **Consensus** - All models agree
|
||||||
|
2. **Majority** - 2/3 agree (note dissent)
|
||||||
|
3. **Unique insights** - Model-specific findings
|
||||||
|
4. **Recommended actions** - Prioritized by consensus
|
||||||
|
|
||||||
|
**Roles:** Each CLI supports `default`, `codereviewer`, `planner`
|
||||||
|
|
|
||||||
36
Status.md
36
Status.md
|
|
@ -1,6 +1,6 @@
|
||||||
# HVAC Community Events - Project Status
|
# HVAC Community Events - Project Status
|
||||||
|
|
||||||
**Last Updated:** February 9, 2026
|
**Last Updated:** February 20, 2026
|
||||||
**Version:** 2.2.18 (Deployed to Production)
|
**Version:** 2.2.18 (Deployed to Production)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -19,7 +19,39 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## CURRENT SESSION - MARKER VISIBILITY TOGGLE CHECKBOX REFACTOR (Feb 9, 2026)
|
## CURRENT SESSION - SLACK NOTIFICATIONS (Feb 20, 2026)
|
||||||
|
|
||||||
|
### Status: COMPLETE - Deployed to Production
|
||||||
|
|
||||||
|
**Objective:** Add Slack notifications for trainer registrations, ticket purchases, and event submissions/publishes via Incoming Webhook with Block Kit rich formatting.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **New Trainer Registration** — name, role, organization, business type, profile photo, "View in WordPress" button
|
||||||
|
- **Ticket Purchase** — purchaser, email, event, ticket count, total, payment gateway, "View Order" button
|
||||||
|
- **Event Submitted by Trainer** — event title, trainer, date, venue, "View Event" button
|
||||||
|
- **Event Published by Admin** — same fields, "View Event" + "Edit in WP" buttons
|
||||||
|
- **Settings UI** — Webhook URL field (password type), "Send Test Notification" button with AJAX feedback
|
||||||
|
- **Test message** includes environment (Staging/Production) and site URL
|
||||||
|
|
||||||
|
### Design Decisions
|
||||||
|
- Non-blocking `wp_remote_post` for all real notifications; blocking only for test button
|
||||||
|
- Atomic `add_post_meta(..., true)` for idempotency (prevents races and duplicate sends)
|
||||||
|
- Webhook URL validated at save-time and send-time: `https` + `hooks.slack.com` + `/services/` path
|
||||||
|
- Graceful degradation: empty webhook = disabled, missing meta = "N/A" fields, no exceptions surface to users
|
||||||
|
- Code reviewed by GPT-5 (Codex) — all 5 findings fixed before production deploy
|
||||||
|
|
||||||
|
### Files Modified
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `includes/class-hvac-slack-notifications.php` | **NEW** — static utility class with 4 notification types + test handler |
|
||||||
|
| `includes/class-hvac-settings.php` | Slack webhook URL setting + validation + test button |
|
||||||
|
| `includes/class-hvac-plugin.php` | Include file + `init()` call in `initializeSecondaryComponents()` |
|
||||||
|
| `includes/class-hvac-registration.php` | One-line call to `notify_new_registration()` after admin email |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PREVIOUS SESSION - MARKER VISIBILITY TOGGLE CHECKBOX REFACTOR (Feb 9, 2026)
|
||||||
|
|
||||||
### Status: COMPLETE - Deployed to Production (v2.2.18)
|
### Status: COMPLETE - Deployed to Production (v2.2.18)
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 339 B After Width: | Height: | Size: 341 B |
Binary file not shown.
|
Before Width: | Height: | Size: 360 B After Width: | Height: | Size: 362 B |
|
|
@ -3,7 +3,7 @@
|
||||||
* Plugin Name: HVAC Community Events
|
* Plugin Name: HVAC Community Events
|
||||||
* Plugin URI: https://upskillhvac.com
|
* Plugin URI: https://upskillhvac.com
|
||||||
* Description: Custom plugin for HVAC trainer event management system
|
* Description: Custom plugin for HVAC trainer event management system
|
||||||
* Version: 2.1.7
|
* Version: 2.2.0
|
||||||
* Author: Upskill HVAC
|
* Author: Upskill HVAC
|
||||||
* Author URI: https://upskillhvac.com
|
* Author URI: https://upskillhvac.com
|
||||||
* License: GPL-2.0+
|
* License: GPL-2.0+
|
||||||
|
|
@ -17,6 +17,50 @@ if (!defined('ABSPATH')) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Staging Email Filter
|
||||||
|
* Prevents emails from being sent to anyone except allowed addresses on staging.
|
||||||
|
* This protects real users from receiving test emails during development.
|
||||||
|
*/
|
||||||
|
function hvac_is_staging_environment() {
|
||||||
|
$host = isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : '';
|
||||||
|
$staging_indicators = array( 'staging', 'upskill-staging', 'localhost', '127.0.0.1' );
|
||||||
|
|
||||||
|
foreach ( $staging_indicators as $indicator ) {
|
||||||
|
if ( stripos( $host, $indicator ) !== false ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hvac_staging_email_filter( $args ) {
|
||||||
|
if ( ! hvac_is_staging_environment() ) {
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed_emails = array( 'ben@tealmaker.com', 'ben@measurequick.com' );
|
||||||
|
$to = $args['to'];
|
||||||
|
|
||||||
|
// Extract email address
|
||||||
|
$email_address = is_array( $to ) ? ( isset( $to[0] ) ? $to[0] : '' ) : $to;
|
||||||
|
if ( preg_match( '/<([^>]+)>/', $email_address, $matches ) ) {
|
||||||
|
$email_address = $matches[1];
|
||||||
|
}
|
||||||
|
$email_address = trim( strtolower( $email_address ) );
|
||||||
|
|
||||||
|
if ( ! in_array( $email_address, $allowed_emails, true ) ) {
|
||||||
|
error_log( sprintf( '[HVAC Staging] Blocked email to: %s | Subject: %s',
|
||||||
|
is_array( $to ) ? implode( ', ', $to ) : $to, $args['subject'] ) );
|
||||||
|
$args['to'] = '';
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
|
||||||
|
$args['subject'] = '[STAGING] ' . $args['subject'];
|
||||||
|
return $args;
|
||||||
|
}
|
||||||
|
add_filter( 'wp_mail', 'hvac_staging_email_filter', 1 );
|
||||||
|
|
||||||
// Load the main plugin class
|
// Load the main plugin class
|
||||||
require_once plugin_dir_path(__FILE__) . 'includes/class-hvac-plugin.php';
|
require_once plugin_dir_path(__FILE__) . 'includes/class-hvac-plugin.php';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1046,6 +1046,15 @@ class HVAC_Ajax_Handlers {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify reCAPTCHA
|
||||||
|
if (class_exists('HVAC_Recaptcha')) {
|
||||||
|
$recaptcha_response = sanitize_text_field($_POST['g-recaptcha-response'] ?? '');
|
||||||
|
if (!HVAC_Recaptcha::instance()->verify_response($recaptcha_response)) {
|
||||||
|
wp_send_json_error(['message' => 'CAPTCHA verification failed. Please try again.'], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Rate limiting - max 5 submissions per IP per hour
|
// Rate limiting - max 5 submissions per IP per hour
|
||||||
$ip = $this->get_client_ip();
|
$ip = $this->get_client_ip();
|
||||||
$rate_key = 'hvac_contact_rate_' . md5($ip);
|
$rate_key = 'hvac_contact_rate_' . md5($ip);
|
||||||
|
|
@ -1202,6 +1211,15 @@ class HVAC_Ajax_Handlers {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify reCAPTCHA
|
||||||
|
if (class_exists('HVAC_Recaptcha')) {
|
||||||
|
$recaptcha_response = sanitize_text_field($_POST['g-recaptcha-response'] ?? '');
|
||||||
|
if (!HVAC_Recaptcha::instance()->verify_response($recaptcha_response)) {
|
||||||
|
wp_send_json_error(['message' => 'CAPTCHA verification failed. Please try again.'], 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Rate limiting - max 5 submissions per IP per hour
|
// Rate limiting - max 5 submissions per IP per hour
|
||||||
$ip = $this->get_client_ip();
|
$ip = $this->get_client_ip();
|
||||||
$rate_key = 'hvac_venue_contact_rate_' . md5($ip);
|
$rate_key = 'hvac_venue_contact_rate_' . md5($ip);
|
||||||
|
|
|
||||||
|
|
@ -291,6 +291,8 @@ class HVAC_Master_Trainers_Overview {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX handler for filtering trainers
|
* AJAX handler for filtering trainers
|
||||||
|
*
|
||||||
|
* Uses get_trainers_table_data() from dashboard data class for reliable trainer listing
|
||||||
*/
|
*/
|
||||||
public function ajax_filter_trainers() {
|
public function ajax_filter_trainers() {
|
||||||
// Verify nonce
|
// Verify nonce
|
||||||
|
|
@ -306,7 +308,6 @@ class HVAC_Master_Trainers_Overview {
|
||||||
// Get filter parameters
|
// Get filter parameters
|
||||||
$args = array(
|
$args = array(
|
||||||
'status' => isset( $_POST['status'] ) ? sanitize_text_field( $_POST['status'] ) : 'all',
|
'status' => isset( $_POST['status'] ) ? sanitize_text_field( $_POST['status'] ) : 'all',
|
||||||
'region' => isset( $_POST['region'] ) ? sanitize_text_field( $_POST['region'] ) : '',
|
|
||||||
'search' => isset( $_POST['search'] ) ? sanitize_text_field( $_POST['search'] ) : '',
|
'search' => isset( $_POST['search'] ) ? sanitize_text_field( $_POST['search'] ) : '',
|
||||||
'page' => isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1,
|
'page' => isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1,
|
||||||
'per_page' => isset( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 20,
|
'per_page' => isset( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 20,
|
||||||
|
|
@ -314,12 +315,21 @@ class HVAC_Master_Trainers_Overview {
|
||||||
'order' => isset( $_POST['order'] ) ? sanitize_text_field( $_POST['order'] ) : 'ASC',
|
'order' => isset( $_POST['order'] ) ? sanitize_text_field( $_POST['order'] ) : 'ASC',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get trainers data
|
// Use the reliable get_trainers_table_data method (same as Master Dashboard)
|
||||||
if ( $this->dashboard_data ) {
|
if ( $this->dashboard_data ) {
|
||||||
$trainer_stats = $this->dashboard_data->get_trainer_statistics();
|
$trainer_data = $this->dashboard_data->get_trainers_table_data( $args );
|
||||||
|
$trainers = $trainer_data['trainers'];
|
||||||
|
|
||||||
// Format trainers for display
|
// Apply region filter if specified (not handled by get_trainers_table_data)
|
||||||
$formatted_trainers = $this->format_trainers_for_display( $trainer_stats['trainer_data'], $args );
|
$region_filter = isset( $_POST['region'] ) ? sanitize_text_field( $_POST['region'] ) : '';
|
||||||
|
if ( ! empty( $region_filter ) ) {
|
||||||
|
$trainers = array_filter( $trainers, function( $trainer ) use ( $region_filter ) {
|
||||||
|
$user_meta = get_user_meta( $trainer['id'] );
|
||||||
|
$user_state = isset( $user_meta['billing_state'][0] ) ? $user_meta['billing_state'][0] : '';
|
||||||
|
return $user_state === $region_filter;
|
||||||
|
} );
|
||||||
|
$trainers = array_values( $trainers ); // Re-index array
|
||||||
|
}
|
||||||
|
|
||||||
// Generate HTML table
|
// Generate HTML table
|
||||||
ob_start();
|
ob_start();
|
||||||
|
|
@ -329,20 +339,20 @@ class HVAC_Master_Trainers_Overview {
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Location</th>
|
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Total Events</th>
|
<th>Total Events</th>
|
||||||
|
<th>Revenue</th>
|
||||||
<th>Registered</th>
|
<th>Registered</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php if ( empty( $formatted_trainers ) ) : ?>
|
<?php if ( empty( $trainers ) ) : ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7" class="hvac-no-results">No trainers found matching your criteria.</td>
|
<td colspan="7" class="hvac-no-results">No trainers found matching your criteria.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<?php foreach ( $formatted_trainers as $trainer ) : ?>
|
<?php foreach ( $trainers as $trainer ) : ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="hvac-trainer-name">
|
<td class="hvac-trainer-name">
|
||||||
<strong><?php echo esc_html( $trainer['name'] ); ?></strong>
|
<strong><?php echo esc_html( $trainer['name'] ); ?></strong>
|
||||||
|
|
@ -352,22 +362,22 @@ class HVAC_Master_Trainers_Overview {
|
||||||
<?php echo esc_html( $trainer['email'] ); ?>
|
<?php echo esc_html( $trainer['email'] ); ?>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="hvac-trainer-location">
|
|
||||||
<?php echo esc_html( $trainer['location'] ); ?>
|
|
||||||
</td>
|
|
||||||
<td class="hvac-trainer-status">
|
<td class="hvac-trainer-status">
|
||||||
<span class="hvac-status-badge <?php echo esc_attr( $trainer['status_class'] ); ?>">
|
<span class="hvac-status-badge hvac-status-<?php echo esc_attr( $trainer['status'] ); ?>">
|
||||||
<?php echo esc_html( $trainer['status'] ); ?>
|
<?php echo esc_html( $trainer['status_label'] ); ?>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="hvac-trainer-events">
|
<td class="hvac-trainer-events">
|
||||||
<?php echo esc_html( $trainer['total_events'] ); ?>
|
<?php echo esc_html( $trainer['total_events'] ); ?>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="hvac-trainer-revenue">
|
||||||
|
$<?php echo esc_html( number_format( $trainer['revenue'], 2 ) ); ?>
|
||||||
|
</td>
|
||||||
<td class="hvac-trainer-registered">
|
<td class="hvac-trainer-registered">
|
||||||
<?php echo esc_html( $trainer['registered'] ); ?>
|
<?php echo esc_html( $trainer['registration_date'] ); ?>
|
||||||
</td>
|
</td>
|
||||||
<td class="hvac-trainer-actions">
|
<td class="hvac-trainer-actions">
|
||||||
<a href="<?php echo esc_url( $trainer['profile_link'] ); ?>"
|
<a href="<?php echo esc_url( home_url( '/master-trainer/trainer-profile/' . $trainer['id'] . '/' ) ); ?>"
|
||||||
class="hvac-btn hvac-btn-sm hvac-btn-primary hvac-view-trainer"
|
class="hvac-btn hvac-btn-sm hvac-btn-primary hvac-view-trainer"
|
||||||
data-trainer-id="<?php echo esc_attr( $trainer['id'] ); ?>">
|
data-trainer-id="<?php echo esc_attr( $trainer['id'] ); ?>">
|
||||||
View Profile
|
View Profile
|
||||||
|
|
@ -379,14 +389,18 @@ class HVAC_Master_Trainers_Overview {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="hvac-trainers-count">
|
<div class="hvac-trainers-count">
|
||||||
Showing <?php echo esc_html( count( $formatted_trainers ) ); ?> trainer(s)
|
Showing <?php echo esc_html( count( $trainers ) ); ?> trainer(s)
|
||||||
|
<?php if ( ! empty( $trainer_data['pagination'] ) ) : ?>
|
||||||
|
of <?php echo esc_html( $trainer_data['pagination']['total_items'] ); ?> total
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
$html = ob_get_clean();
|
$html = ob_get_clean();
|
||||||
|
|
||||||
wp_send_json_success( array(
|
wp_send_json_success( array(
|
||||||
'html' => $html,
|
'html' => $html,
|
||||||
'total_found' => count( $formatted_trainers )
|
'total_found' => count( $trainers ),
|
||||||
|
'pagination' => $trainer_data['pagination']
|
||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -514,16 +528,35 @@ class HVAC_Master_Trainers_Overview {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Count trainers by status
|
* Count trainers by status
|
||||||
|
*
|
||||||
|
* Uses HVAC_Trainer_Status for dynamic status calculation (active/inactive based on event activity)
|
||||||
*/
|
*/
|
||||||
private function count_trainers_by_status( $status ) {
|
private function count_trainers_by_status( $status ) {
|
||||||
|
// Load trainer status class for dynamic status calculation
|
||||||
|
if ( ! class_exists( 'HVAC_Trainer_Status' ) ) {
|
||||||
|
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-trainer-status.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all trainers (both roles)
|
||||||
$users = get_users( array(
|
$users = get_users( array(
|
||||||
'role' => 'hvac_trainer',
|
'role__in' => array( 'hvac_trainer', 'hvac_master_trainer' ),
|
||||||
'meta_key' => 'hvac_trainer_status',
|
'fields' => 'ID'
|
||||||
'meta_value' => $status,
|
|
||||||
'count_total' => true
|
|
||||||
) );
|
) );
|
||||||
|
|
||||||
return is_array( $users ) ? count( $users ) : 0;
|
if ( empty( $users ) ) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count by dynamic status
|
||||||
|
$count = 0;
|
||||||
|
foreach ( $users as $user_id ) {
|
||||||
|
$user_status = HVAC_Trainer_Status::get_trainer_status( $user_id );
|
||||||
|
if ( $user_status === $status ) {
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,7 @@ final class HVAC_Plugin {
|
||||||
'class-attendee-profile.php',
|
'class-attendee-profile.php',
|
||||||
'class-hvac-page-content-fixer.php',
|
'class-hvac-page-content-fixer.php',
|
||||||
'class-hvac-page-content-manager.php',
|
'class-hvac-page-content-manager.php',
|
||||||
|
'class-hvac-slack-notifications.php',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Find a Trainer feature files
|
// Find a Trainer feature files
|
||||||
|
|
@ -759,6 +760,11 @@ final class HVAC_Plugin {
|
||||||
if (class_exists('HVAC_Announcements_Display')) {
|
if (class_exists('HVAC_Announcements_Display')) {
|
||||||
HVAC_Announcements_Display::get_instance();
|
HVAC_Announcements_Display::get_instance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize Slack notifications (registration + ticket purchase hooks)
|
||||||
|
if (class_exists('HVAC_Slack_Notifications')) {
|
||||||
|
HVAC_Slack_Notifications::init();
|
||||||
|
}
|
||||||
error_log('HVAC Plugin: Checking if HVAC_Announcements_Admin class exists: ' . (class_exists('HVAC_Announcements_Admin') ? 'YES' : 'NO'));
|
error_log('HVAC Plugin: Checking if HVAC_Announcements_Admin class exists: ' . (class_exists('HVAC_Announcements_Admin') ? 'YES' : 'NO'));
|
||||||
if (class_exists('HVAC_Announcements_Admin')) {
|
if (class_exists('HVAC_Announcements_Admin')) {
|
||||||
error_log('HVAC Plugin: Instantiating HVAC_Announcements_Admin...');
|
error_log('HVAC Plugin: Instantiating HVAC_Announcements_Admin...');
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,11 @@ class HVAC_Registration {
|
||||||
$this->send_admin_notification($user_id, $submitted_data);
|
$this->send_admin_notification($user_id, $submitted_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Slack notification (non-blocking, fire-and-forget)
|
||||||
|
if (class_exists('HVAC_Slack_Notifications')) {
|
||||||
|
HVAC_Slack_Notifications::notify_new_registration($user_id, $submitted_data);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Success Redirect ---
|
// --- Success Redirect ---
|
||||||
$success_redirect_url = home_url('/registration-pending/'); // URL from E2E test
|
$success_redirect_url = home_url('/registration-pending/'); // URL from E2E test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,15 @@ class HVAC_Settings {
|
||||||
|
|
||||||
public function register_settings() {
|
public function register_settings() {
|
||||||
register_setting('hvac_ce_options', 'hvac_ce_options');
|
register_setting('hvac_ce_options', 'hvac_ce_options');
|
||||||
|
register_setting('hvac_ce_options', 'hvac_google_maps_api_key', [
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
]);
|
||||||
|
register_setting('hvac_ce_options', 'hvac_google_geocoding_api_key', [
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
|
]);
|
||||||
|
|
||||||
add_settings_section(
|
add_settings_section(
|
||||||
'hvac_ce_main',
|
'hvac_ce_main',
|
||||||
__('HVAC Community Events Settings', 'hvac-ce'),
|
__('HVAC Community Events Settings', 'hvac-ce'),
|
||||||
|
|
@ -78,6 +86,136 @@ class HVAC_Settings {
|
||||||
'hvac-ce',
|
'hvac-ce',
|
||||||
'hvac_ce_main'
|
'hvac_ce_main'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Google Maps API Settings Section
|
||||||
|
add_settings_section(
|
||||||
|
'hvac_ce_google_maps',
|
||||||
|
__('Google Maps API Settings', 'hvac-ce'),
|
||||||
|
[$this, 'google_maps_section_callback'],
|
||||||
|
'hvac-ce'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'hvac_google_maps_api_key',
|
||||||
|
__('Maps JavaScript API Key', 'hvac-ce'),
|
||||||
|
[$this, 'maps_api_key_callback'],
|
||||||
|
'hvac-ce',
|
||||||
|
'hvac_ce_google_maps'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'hvac_google_geocoding_api_key',
|
||||||
|
__('Geocoding API Key', 'hvac-ce'),
|
||||||
|
[$this, 'geocoding_api_key_callback'],
|
||||||
|
'hvac-ce',
|
||||||
|
'hvac_ce_google_maps'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Slack Integration Section
|
||||||
|
register_setting('hvac_ce_options', 'hvac_slack_webhook_url', [
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => [$this, 'sanitize_slack_webhook_url'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
add_settings_section(
|
||||||
|
'hvac_ce_slack',
|
||||||
|
__('Slack Integration', 'hvac-ce'),
|
||||||
|
[$this, 'slack_section_callback'],
|
||||||
|
'hvac-ce'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_settings_field(
|
||||||
|
'hvac_slack_webhook_url',
|
||||||
|
__('Webhook URL', 'hvac-ce'),
|
||||||
|
[$this, 'slack_webhook_url_callback'],
|
||||||
|
'hvac-ce',
|
||||||
|
'hvac_ce_slack'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize Slack webhook URL — only allow hooks.slack.com
|
||||||
|
*/
|
||||||
|
public function sanitize_slack_webhook_url(mixed $value): string {
|
||||||
|
if (!is_string($value)) {
|
||||||
|
return get_option('hvac_slack_webhook_url', '');
|
||||||
|
}
|
||||||
|
$value = trim($value);
|
||||||
|
if ($value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = esc_url_raw($value);
|
||||||
|
$scheme = parse_url($value, PHP_URL_SCHEME);
|
||||||
|
$host = parse_url($value, PHP_URL_HOST);
|
||||||
|
$path = parse_url($value, PHP_URL_PATH) ?: '';
|
||||||
|
|
||||||
|
if ($scheme !== 'https' || $host !== 'hooks.slack.com' || !str_starts_with($path, '/services/')) {
|
||||||
|
add_settings_error(
|
||||||
|
'hvac_slack_webhook_url',
|
||||||
|
'invalid_url',
|
||||||
|
__('Slack webhook URL must be a valid https://hooks.slack.com/services/... URL.', 'hvac-ce'),
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
return get_option('hvac_slack_webhook_url', ''); // keep old value
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function slack_section_callback() {
|
||||||
|
echo '<p>' . __('Sends notifications for new trainer registrations and ticket purchases to a Slack channel.', 'hvac-ce') . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function slack_webhook_url_callback() {
|
||||||
|
$value = get_option('hvac_slack_webhook_url', '');
|
||||||
|
echo '<input type="password" name="hvac_slack_webhook_url" value="' . esc_attr($value) . '" class="regular-text" placeholder="https://hooks.slack.com/services/...">';
|
||||||
|
echo '<p class="description">' . __('Create an Incoming Webhook in your Slack workspace and paste the URL here. Leave empty to disable.', 'hvac-ce') . '</p>';
|
||||||
|
|
||||||
|
if (!empty($value)) {
|
||||||
|
$nonce = wp_create_nonce('hvac_test_slack_webhook');
|
||||||
|
echo '<div style="margin-top: 10px;">';
|
||||||
|
echo '<button type="button" id="hvac-test-slack-btn" class="button button-secondary" data-nonce="' . esc_attr($nonce) . '">';
|
||||||
|
echo __('Send Test Notification', 'hvac-ce');
|
||||||
|
echo '</button>';
|
||||||
|
echo '<span id="hvac-slack-test-status" style="margin-left: 10px;"></span>';
|
||||||
|
echo '</div>';
|
||||||
|
?>
|
||||||
|
<script type="text/javascript">
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
$('#hvac-test-slack-btn').on('click', function() {
|
||||||
|
var $btn = $(this);
|
||||||
|
var $status = $('#hvac-slack-test-status');
|
||||||
|
var nonce = $btn.data('nonce');
|
||||||
|
|
||||||
|
$btn.prop('disabled', true).text('<?php echo esc_js(__('Sending...', 'hvac-ce')); ?>');
|
||||||
|
$status.html('');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: ajaxurl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'hvac_test_slack_webhook',
|
||||||
|
nonce: nonce
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
$status.html('<span style="color: green;">\u2713 ' + response.data.message + '</span>');
|
||||||
|
} else {
|
||||||
|
$status.html('<span style="color: red;">\u2717 ' + (response.data?.message || 'Unknown error') + '</span>');
|
||||||
|
}
|
||||||
|
$btn.prop('disabled', false).text('<?php echo esc_js(__('Send Test Notification', 'hvac-ce')); ?>');
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$status.html('<span style="color: red;">\u2717 <?php echo esc_js(__('Network error. Please try again.', 'hvac-ce')); ?></span>');
|
||||||
|
$btn.prop('disabled', false).text('<?php echo esc_js(__('Send Test Notification', 'hvac-ce')); ?>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function settings_section_callback() {
|
public function settings_section_callback() {
|
||||||
|
|
@ -86,12 +224,318 @@ class HVAC_Settings {
|
||||||
|
|
||||||
public function notification_emails_callback() {
|
public function notification_emails_callback() {
|
||||||
$options = get_option('hvac_ce_options');
|
$options = get_option('hvac_ce_options');
|
||||||
echo '<input type="text" name="hvac_ce_options[notification_emails]" value="' .
|
echo '<input type="text" name="hvac_ce_options[notification_emails]" value="' .
|
||||||
esc_attr($options['notification_emails'] ?? '') . '" class="regular-text">';
|
esc_attr($options['notification_emails'] ?? '') . '" class="regular-text">';
|
||||||
echo '<p class="description">' .
|
echo '<p class="description">' .
|
||||||
__('Comma-separated list of emails to notify when new trainers register', 'hvac-ce') . '</p>';
|
__('Comma-separated list of emails to notify when new trainers register', 'hvac-ce') . '</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function google_maps_section_callback() {
|
||||||
|
echo '<p>' . __('Configure Google Maps API keys for maps and geocoding functionality.', 'hvac-ce') . '</p>';
|
||||||
|
echo '<p class="description">' . __('You need two API keys: one for browser-side Maps JavaScript API (HTTP referrer restricted) and one for server-side Geocoding API (IP restricted).', 'hvac-ce') . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function maps_api_key_callback() {
|
||||||
|
$value = get_option('hvac_google_maps_api_key', '');
|
||||||
|
echo '<input type="text" name="hvac_google_maps_api_key" value="' . esc_attr($value) . '" class="regular-text" placeholder="AIza...">';
|
||||||
|
echo '<p class="description">' . __('Browser-side API key for Google Maps JavaScript API. Should be HTTP referrer restricted to your domain.', 'hvac-ce') . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function geocoding_api_key_callback() {
|
||||||
|
$value = get_option('hvac_google_geocoding_api_key', '');
|
||||||
|
echo '<input type="text" name="hvac_google_geocoding_api_key" value="' . esc_attr($value) . '" class="regular-text" placeholder="AIza...">';
|
||||||
|
echo '<p class="description">' . __('Server-side API key for Google Geocoding API. Should be IP restricted to your server IP address.', 'hvac-ce') . '</p>';
|
||||||
|
|
||||||
|
// Show current status
|
||||||
|
if (!empty($value)) {
|
||||||
|
echo '<p style="color: green;">✓ ' . __('Geocoding API key is configured.', 'hvac-ce') . '</p>';
|
||||||
|
|
||||||
|
// Show batch geocode button
|
||||||
|
$this->render_batch_geocode_button();
|
||||||
|
} else {
|
||||||
|
echo '<p style="color: orange;">⚠ ' . __('Geocoding API key not set. Venue addresses will not be geocoded for map display.', 'hvac-ce') . '</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render batch geocode button and status
|
||||||
|
*/
|
||||||
|
private function render_batch_geocode_button() {
|
||||||
|
// Count venues without coordinates
|
||||||
|
$venues_without_coords = get_posts([
|
||||||
|
'post_type' => 'tribe_venue',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'fields' => 'ids',
|
||||||
|
'meta_query' => [
|
||||||
|
'relation' => 'AND',
|
||||||
|
[
|
||||||
|
'key' => 'venue_latitude',
|
||||||
|
'compare' => 'NOT EXISTS'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => '_VenueLat',
|
||||||
|
'compare' => 'NOT EXISTS'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$count = count($venues_without_coords);
|
||||||
|
$nonce = wp_create_nonce('hvac_batch_geocode_venues');
|
||||||
|
|
||||||
|
echo '<div style="margin-top: 15px; padding: 15px; background: #f5f5f5; border-radius: 4px;">';
|
||||||
|
echo '<strong>' . __('Venue Geocoding Status', 'hvac-ce') . '</strong><br>';
|
||||||
|
|
||||||
|
if ($count > 0) {
|
||||||
|
echo '<p style="color: orange;">' . sprintf(__('%d venues need geocoding.', 'hvac-ce'), $count) . '</p>';
|
||||||
|
echo '<button type="button" id="hvac-batch-geocode-btn" class="button button-secondary" data-nonce="' . esc_attr($nonce) . '">';
|
||||||
|
echo __('Geocode All Venues', 'hvac-ce');
|
||||||
|
echo '</button>';
|
||||||
|
echo '<span id="hvac-geocode-status" style="margin-left: 10px;"></span>';
|
||||||
|
} else {
|
||||||
|
echo '<p style="color: green;">✓ ' . __('All venues are geocoded.', 'hvac-ce') . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
// Render the mark as approved labs button
|
||||||
|
$this->render_mark_approved_button();
|
||||||
|
|
||||||
|
// Add inline JavaScript for the batch geocode button
|
||||||
|
?>
|
||||||
|
<script type="text/javascript">
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
$('#hvac-batch-geocode-btn').on('click', function() {
|
||||||
|
var $btn = $(this);
|
||||||
|
var $status = $('#hvac-geocode-status');
|
||||||
|
var nonce = $btn.data('nonce');
|
||||||
|
|
||||||
|
$btn.prop('disabled', true).text('<?php echo esc_js(__('Processing...', 'hvac-ce')); ?>');
|
||||||
|
$status.html('<span style="color: blue;"><?php echo esc_js(__('Geocoding venues...', 'hvac-ce')); ?></span>');
|
||||||
|
|
||||||
|
function processNextBatch() {
|
||||||
|
$.ajax({
|
||||||
|
url: ajaxurl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'hvac_batch_geocode_venues',
|
||||||
|
nonce: nonce,
|
||||||
|
limit: 10
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
var data = response.data;
|
||||||
|
var msg = '<?php echo esc_js(__('Processed', 'hvac-ce')); ?>: ' + data.processed +
|
||||||
|
', <?php echo esc_js(__('Success', 'hvac-ce')); ?>: ' + data.success +
|
||||||
|
', <?php echo esc_js(__('Remaining', 'hvac-ce')); ?>: ' + data.remaining;
|
||||||
|
$status.html('<span style="color: blue;">' + msg + '</span>');
|
||||||
|
|
||||||
|
if (data.remaining > 0) {
|
||||||
|
// Continue with next batch
|
||||||
|
setTimeout(processNextBatch, 1000);
|
||||||
|
} else {
|
||||||
|
$status.html('<span style="color: green;">✓ <?php echo esc_js(__('All venues geocoded!', 'hvac-ce')); ?></span>');
|
||||||
|
$btn.text('<?php echo esc_js(__('Done!', 'hvac-ce')); ?>');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$status.html('<span style="color: red;"><?php echo esc_js(__('Error', 'hvac-ce')); ?>: ' + (response.data?.message || 'Unknown error') + '</span>');
|
||||||
|
$btn.prop('disabled', false).text('<?php echo esc_js(__('Retry', 'hvac-ce')); ?>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$status.html('<span style="color: red;"><?php echo esc_js(__('Network error. Please try again.', 'hvac-ce')); ?></span>');
|
||||||
|
$btn.prop('disabled', false).text('<?php echo esc_js(__('Retry', 'hvac-ce')); ?>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
processNextBatch();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render scrollable list of venues with checkboxes for approved training labs
|
||||||
|
*/
|
||||||
|
private function render_mark_approved_button() {
|
||||||
|
// Get all venues
|
||||||
|
$all_venues = get_posts([
|
||||||
|
'post_type' => 'tribe_venue',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$nonce = wp_create_nonce('hvac_mark_venues_approved');
|
||||||
|
|
||||||
|
echo '<div style="margin-top: 15px; padding: 15px; background: #f0f7ff; border-radius: 4px; border-left: 4px solid #0073aa;">';
|
||||||
|
echo '<strong>' . __('measureQuick Approved Training Labs', 'hvac-ce') . '</strong><br>';
|
||||||
|
echo '<p class="description">' . __('Select which venues should appear on the Find Training map as approved training labs.', 'hvac-ce') . '</p>';
|
||||||
|
|
||||||
|
if (empty($all_venues)) {
|
||||||
|
echo '<p style="color: gray;">' . __('No venues found.', 'hvac-ce') . '</p>';
|
||||||
|
echo '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build venue data
|
||||||
|
$venue_data = [];
|
||||||
|
$approved_count = 0;
|
||||||
|
$geocoded_count = 0;
|
||||||
|
|
||||||
|
foreach ($all_venues as $venue) {
|
||||||
|
$is_approved = has_term('mq-approved-lab', 'venue_type', $venue->ID);
|
||||||
|
$lat = get_post_meta($venue->ID, 'venue_latitude', true) ?: get_post_meta($venue->ID, '_VenueLat', true);
|
||||||
|
$lng = get_post_meta($venue->ID, 'venue_longitude', true) ?: get_post_meta($venue->ID, '_VenueLng', true);
|
||||||
|
$has_coords = !empty($lat) && !empty($lng);
|
||||||
|
|
||||||
|
$city = get_post_meta($venue->ID, '_VenueCity', true);
|
||||||
|
$state = get_post_meta($venue->ID, '_VenueState', true);
|
||||||
|
$location = trim($city . ($city && $state ? ', ' : '') . $state);
|
||||||
|
|
||||||
|
if ($is_approved) $approved_count++;
|
||||||
|
if ($has_coords) $geocoded_count++;
|
||||||
|
|
||||||
|
$venue_data[] = [
|
||||||
|
'id' => $venue->ID,
|
||||||
|
'title' => $venue->post_title,
|
||||||
|
'location' => $location,
|
||||||
|
'is_approved' => $is_approved,
|
||||||
|
'has_coords' => $has_coords
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
echo '<p style="margin: 10px 0;">';
|
||||||
|
echo sprintf(__('<strong>%d</strong> venues total, <strong>%d</strong> approved, <strong>%d</strong> geocoded', 'hvac-ce'),
|
||||||
|
count($all_venues), $approved_count, $geocoded_count);
|
||||||
|
echo '</p>';
|
||||||
|
|
||||||
|
// Scrollable list
|
||||||
|
echo '<div style="max-height: 300px; overflow-y: auto; border: 1px solid #ddd; background: #fff; padding: 10px; margin: 10px 0;">';
|
||||||
|
echo '<table style="width: 100%; border-collapse: collapse;">';
|
||||||
|
echo '<thead style="position: sticky; top: 0; background: #f5f5f5;">';
|
||||||
|
echo '<tr>';
|
||||||
|
echo '<th style="padding: 8px; text-align: left; border-bottom: 2px solid #ddd; width: 30px;">';
|
||||||
|
echo '<input type="checkbox" id="hvac-select-all-venues" title="' . esc_attr__('Select/Deselect All', 'hvac-ce') . '">';
|
||||||
|
echo '</th>';
|
||||||
|
echo '<th style="padding: 8px; text-align: left; border-bottom: 2px solid #ddd;">' . __('Venue', 'hvac-ce') . '</th>';
|
||||||
|
echo '<th style="padding: 8px; text-align: left; border-bottom: 2px solid #ddd;">' . __('Location', 'hvac-ce') . '</th>';
|
||||||
|
echo '<th style="padding: 8px; text-align: center; border-bottom: 2px solid #ddd;">' . __('Geocoded', 'hvac-ce') . '</th>';
|
||||||
|
echo '</tr>';
|
||||||
|
echo '</thead>';
|
||||||
|
echo '<tbody>';
|
||||||
|
|
||||||
|
foreach ($venue_data as $venue) {
|
||||||
|
$row_style = $venue['is_approved'] ? 'background: #e7f5e7;' : '';
|
||||||
|
echo '<tr style="' . $row_style . '">';
|
||||||
|
echo '<td style="padding: 6px 8px; border-bottom: 1px solid #eee;">';
|
||||||
|
echo '<input type="checkbox" class="hvac-venue-checkbox" value="' . esc_attr($venue['id']) . '"' .
|
||||||
|
($venue['is_approved'] ? ' checked' : '') . '>';
|
||||||
|
echo '</td>';
|
||||||
|
echo '<td style="padding: 6px 8px; border-bottom: 1px solid #eee;">' . esc_html($venue['title']) . '</td>';
|
||||||
|
echo '<td style="padding: 6px 8px; border-bottom: 1px solid #eee; color: #666;">' . esc_html($venue['location'] ?: '—') . '</td>';
|
||||||
|
echo '<td style="padding: 6px 8px; border-bottom: 1px solid #eee; text-align: center;">';
|
||||||
|
echo $venue['has_coords'] ? '<span style="color: green;">✓</span>' : '<span style="color: #ccc;">—</span>';
|
||||||
|
echo '</td>';
|
||||||
|
echo '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</tbody>';
|
||||||
|
echo '</table>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
// Save button
|
||||||
|
echo '<button type="button" id="hvac-save-approved-labs" class="button button-primary" data-nonce="' . esc_attr($nonce) . '">';
|
||||||
|
echo __('Save Approved Labs', 'hvac-ce');
|
||||||
|
echo '</button>';
|
||||||
|
echo '<span id="hvac-approved-status" style="margin-left: 10px;"></span>';
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
// JavaScript
|
||||||
|
?>
|
||||||
|
<script type="text/javascript">
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
// Select all checkbox
|
||||||
|
$('#hvac-select-all-venues').on('change', function() {
|
||||||
|
$('.hvac-venue-checkbox').prop('checked', $(this).is(':checked'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update select-all state when individual checkboxes change
|
||||||
|
$('.hvac-venue-checkbox').on('change', function() {
|
||||||
|
var total = $('.hvac-venue-checkbox').length;
|
||||||
|
var checked = $('.hvac-venue-checkbox:checked').length;
|
||||||
|
$('#hvac-select-all-venues').prop('checked', total === checked);
|
||||||
|
$('#hvac-select-all-venues').prop('indeterminate', checked > 0 && checked < total);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize select-all state
|
||||||
|
(function() {
|
||||||
|
var total = $('.hvac-venue-checkbox').length;
|
||||||
|
var checked = $('.hvac-venue-checkbox:checked').length;
|
||||||
|
$('#hvac-select-all-venues').prop('checked', total === checked);
|
||||||
|
$('#hvac-select-all-venues').prop('indeterminate', checked > 0 && checked < total);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Save button
|
||||||
|
$('#hvac-save-approved-labs').on('click', function() {
|
||||||
|
var $btn = $(this);
|
||||||
|
var $status = $('#hvac-approved-status');
|
||||||
|
var nonce = $btn.data('nonce');
|
||||||
|
|
||||||
|
// Collect selected venue IDs
|
||||||
|
var selectedIds = [];
|
||||||
|
$('.hvac-venue-checkbox:checked').each(function() {
|
||||||
|
selectedIds.push($(this).val());
|
||||||
|
});
|
||||||
|
|
||||||
|
$btn.prop('disabled', true).text('<?php echo esc_js(__('Saving...', 'hvac-ce')); ?>');
|
||||||
|
$status.html('<span style="color: blue;"><?php echo esc_js(__('Updating venues...', 'hvac-ce')); ?></span>');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: ajaxurl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'hvac_update_approved_labs',
|
||||||
|
nonce: nonce,
|
||||||
|
venue_ids: selectedIds
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
var data = response.data;
|
||||||
|
var msg = '<?php echo esc_js(__('Saved!', 'hvac-ce')); ?> ' + data.approved_count + ' <?php echo esc_js(__('approved labs', 'hvac-ce')); ?>';
|
||||||
|
$status.html('<span style="color: green;">✓ ' + msg + '</span>');
|
||||||
|
$btn.prop('disabled', false).text('<?php echo esc_js(__('Save Approved Labs', 'hvac-ce')); ?>');
|
||||||
|
|
||||||
|
// Update row highlighting
|
||||||
|
$('.hvac-venue-checkbox').each(function() {
|
||||||
|
var $row = $(this).closest('tr');
|
||||||
|
if ($(this).is(':checked')) {
|
||||||
|
$row.css('background', '#e7f5e7');
|
||||||
|
} else {
|
||||||
|
$row.css('background', '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$status.html('<span style="color: red;"><?php echo esc_js(__('Error', 'hvac-ce')); ?>: ' + (response.data?.message || 'Unknown error') + '</span>');
|
||||||
|
$btn.prop('disabled', false).text('<?php echo esc_js(__('Save Approved Labs', 'hvac-ce')); ?>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$status.html('<span style="color: red;"><?php echo esc_js(__('Network error. Please try again.', 'hvac-ce')); ?></span>');
|
||||||
|
$btn.prop('disabled', false).text('<?php echo esc_js(__('Save Approved Labs', 'hvac-ce')); ?>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
public function options_page() {
|
public function options_page() {
|
||||||
$active_tab = isset( $_GET['tab'] ) ? sanitize_text_field( $_GET['tab'] ) : 'general';
|
$active_tab = isset( $_GET['tab'] ) ? sanitize_text_field( $_GET['tab'] ) : 'general';
|
||||||
|
|
||||||
|
|
|
||||||
485
includes/class-hvac-slack-notifications.php
Normal file
485
includes/class-hvac-slack-notifications.php
Normal file
|
|
@ -0,0 +1,485 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slack notification utility for trainer registrations and ticket purchases.
|
||||||
|
*
|
||||||
|
* Uses Slack Incoming Webhooks with Block Kit rich formatting.
|
||||||
|
* All sends are non-blocking (fire-and-forget) so Slack failures
|
||||||
|
* never affect registration or checkout flows.
|
||||||
|
*/
|
||||||
|
class HVAC_Slack_Notifications {
|
||||||
|
|
||||||
|
/** @var bool Prevents duplicate hook registration */
|
||||||
|
private static bool $initialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register hooks. Called once from HVAC_Plugin::initializeSecondaryComponents().
|
||||||
|
*/
|
||||||
|
public static function init(): void {
|
||||||
|
if (self::$initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self::$initialized = true;
|
||||||
|
|
||||||
|
// Ticket purchase: fires on any post status transition
|
||||||
|
add_action('transition_post_status', [__CLASS__, 'on_order_status_change'], 10, 3);
|
||||||
|
|
||||||
|
// Event published by admin (any tribe_events post transitioning to publish)
|
||||||
|
add_action('transition_post_status', [__CLASS__, 'on_event_status_change'], 10, 3);
|
||||||
|
|
||||||
|
// Event submitted by trainer via TEC Community Events form
|
||||||
|
add_action('hvac_tec_event_saved', [__CLASS__, 'notify_event_submitted'], 10, 1);
|
||||||
|
|
||||||
|
// AJAX: test webhook from settings page
|
||||||
|
add_action('wp_ajax_hvac_test_slack_webhook', [__CLASS__, 'send_test_notification']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to Slack via Incoming Webhook.
|
||||||
|
*
|
||||||
|
* @param string $text Plain-text fallback (required by Slack).
|
||||||
|
* @param array $blocks Block Kit blocks for rich layout.
|
||||||
|
* @param bool $blocking Whether to wait for the response.
|
||||||
|
* @return bool|null True on success, false on failure, null if disabled.
|
||||||
|
*/
|
||||||
|
private static function send(string $text, array $blocks, bool $blocking = false): ?bool {
|
||||||
|
$webhook_url = get_option('hvac_slack_webhook_url', '');
|
||||||
|
|
||||||
|
if (empty($webhook_url)) {
|
||||||
|
return null; // Disabled — no webhook configured
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate URL: must be https://hooks.slack.com/services/...
|
||||||
|
$scheme = parse_url($webhook_url, PHP_URL_SCHEME);
|
||||||
|
$host = parse_url($webhook_url, PHP_URL_HOST);
|
||||||
|
$path = parse_url($webhook_url, PHP_URL_PATH) ?: '';
|
||||||
|
if ($scheme !== 'https' || $host !== 'hooks.slack.com' || !str_starts_with($path, '/services/')) {
|
||||||
|
error_log('[HVAC Slack] Rejected webhook URL: ' . $webhook_url);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = wp_json_encode([
|
||||||
|
'text' => $text,
|
||||||
|
'blocks' => $blocks,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = wp_remote_post($webhook_url, [
|
||||||
|
'body' => $payload,
|
||||||
|
'headers' => ['Content-Type' => 'application/json'],
|
||||||
|
'timeout' => 5,
|
||||||
|
'blocking' => $blocking,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($blocking) {
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
error_log('[HVAC Slack] Send failed: ' . $response->get_error_message());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$code = wp_remote_retrieve_response_code($response);
|
||||||
|
if ($code !== 200) {
|
||||||
|
$body = wp_remote_retrieve_body($response);
|
||||||
|
error_log("[HVAC Slack] Slack returned HTTP {$code}: {$body}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-blocking — optimistically assume success
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
error_log('[HVAC Slack] Non-blocking send error: ' . $response->get_error_message());
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Registration notification
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a Slack notification for a new trainer registration.
|
||||||
|
*
|
||||||
|
* @param int $user_id Newly created user ID.
|
||||||
|
* @param array $submitted_data Raw form data from registration.
|
||||||
|
*/
|
||||||
|
public static function notify_new_registration(int $user_id, array $submitted_data): void {
|
||||||
|
$first_name = sanitize_text_field($submitted_data['first_name'] ?? 'N/A');
|
||||||
|
$last_name = sanitize_text_field($submitted_data['last_name'] ?? '');
|
||||||
|
$full_name = trim("{$first_name} {$last_name}") ?: 'N/A';
|
||||||
|
$role = sanitize_text_field($submitted_data['role'] ?? $submitted_data['trainer_type'] ?? 'Trainer');
|
||||||
|
$business_name = sanitize_text_field($submitted_data['business_name'] ?? $submitted_data['company'] ?? 'N/A');
|
||||||
|
$business_type = sanitize_text_field($submitted_data['business_type'] ?? 'N/A');
|
||||||
|
|
||||||
|
$admin_url = admin_url("user-edit.php?user_id={$user_id}");
|
||||||
|
$text = "New Trainer Registration: {$full_name} ({$role})";
|
||||||
|
|
||||||
|
// Profile image accessory (optional)
|
||||||
|
$accessory = null;
|
||||||
|
$image_id = get_user_meta($user_id, 'profile_image_id', true);
|
||||||
|
if ($image_id) {
|
||||||
|
$image_url = wp_get_attachment_image_url((int) $image_id, 'thumbnail');
|
||||||
|
if ($image_url) {
|
||||||
|
$accessory = [
|
||||||
|
'type' => 'image',
|
||||||
|
'image_url' => $image_url,
|
||||||
|
'alt_text' => $full_name,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields_section = [
|
||||||
|
'type' => 'section',
|
||||||
|
'fields' => [
|
||||||
|
['type' => 'mrkdwn', 'text' => "*Name:*\n{$full_name}"],
|
||||||
|
['type' => 'mrkdwn', 'text' => "*Role:*\n{$role}"],
|
||||||
|
['type' => 'mrkdwn', 'text' => "*Organization:*\n{$business_name}"],
|
||||||
|
['type' => 'mrkdwn', 'text' => "*Business Type:*\n{$business_type}"],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($accessory) {
|
||||||
|
$fields_section['accessory'] = $accessory;
|
||||||
|
}
|
||||||
|
|
||||||
|
$blocks = [
|
||||||
|
[
|
||||||
|
'type' => 'header',
|
||||||
|
'text' => ['type' => 'plain_text', 'text' => "\xF0\x9F\x86\x95 New Trainer Registration", 'emoji' => true],
|
||||||
|
],
|
||||||
|
$fields_section,
|
||||||
|
[
|
||||||
|
'type' => 'actions',
|
||||||
|
'elements' => [
|
||||||
|
[
|
||||||
|
'type' => 'button',
|
||||||
|
'text' => ['type' => 'plain_text', 'text' => 'View in WordPress'],
|
||||||
|
'url' => $admin_url,
|
||||||
|
'style' => 'primary',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
self::send($text, $blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Ticket purchase notification
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook callback for transition_post_status.
|
||||||
|
*
|
||||||
|
* @param string $new_status New post status.
|
||||||
|
* @param string $old_status Previous post status.
|
||||||
|
* @param WP_Post $post Post object.
|
||||||
|
*/
|
||||||
|
public static function on_order_status_change(string $new_status, string $old_status, \WP_Post $post): void {
|
||||||
|
// Guard 1: correct post type
|
||||||
|
if ($post->post_type !== 'tec_tc_order') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard 2: transitioning INTO completed
|
||||||
|
if ($new_status !== 'tec-tc-completed') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard 3: not already completed (prevents re-saves)
|
||||||
|
if ($old_status === 'tec-tc-completed') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomic idempotency: add_post_meta with $unique=true prevents races
|
||||||
|
if (!add_post_meta($post->ID, '_hvac_slack_ticket_notified', '1', true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::notify_ticket_purchase($post->ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a Slack notification for a completed ticket purchase.
|
||||||
|
*
|
||||||
|
* @param int $order_id TEC Tickets Commerce order post ID.
|
||||||
|
*/
|
||||||
|
public static function notify_ticket_purchase(int $order_id): void {
|
||||||
|
$purchaser_name = get_post_meta($order_id, '_tec_tc_order_purchaser_name', true) ?: 'N/A';
|
||||||
|
$purchaser_email = get_post_meta($order_id, '_tec_tc_order_purchaser_email', true) ?: 'N/A';
|
||||||
|
$order_items = get_post_meta($order_id, '_tec_tc_order_items', true);
|
||||||
|
$gateway = get_post_meta($order_id, '_tec_tc_order_gateway', true) ?: 'Unknown';
|
||||||
|
|
||||||
|
$total_qty = 0;
|
||||||
|
$total_price = 0.0;
|
||||||
|
$event_title = 'N/A';
|
||||||
|
|
||||||
|
if (is_array($order_items)) {
|
||||||
|
foreach ($order_items as $item) {
|
||||||
|
$qty = (int) ($item['quantity'] ?? 1);
|
||||||
|
$total_qty += $qty;
|
||||||
|
$total_price += (float) ($item['sub_total'] ?? $item['price'] ?? 0) * $qty;
|
||||||
|
|
||||||
|
if (!empty($item['event_id']) && $event_title === 'N/A') {
|
||||||
|
$title = get_the_title((int) $item['event_id']);
|
||||||
|
if ($title) {
|
||||||
|
$event_title = $title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$total_formatted = '$' . number_format($total_price, 2);
|
||||||
|
$admin_url = admin_url("post.php?post={$order_id}&action=edit");
|
||||||
|
$text = "New Ticket Purchase: {$purchaser_name} — {$total_qty} ticket(s) for {$event_title} ({$total_formatted})";
|
||||||
|
|
||||||
|
$blocks = [
|
||||||
|
[
|
||||||
|
'type' => 'header',
|
||||||
|
'text' => ['type' => 'plain_text', 'text' => "\xF0\x9F\x8E\x9F\xEF\xB8\x8F New Ticket Purchase", 'emoji' => true],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'section',
|
||||||
|
'fields' => [
|
||||||
|
['type' => 'mrkdwn', 'text' => "*Purchaser:*\n{$purchaser_name}"],
|
||||||
|
['type' => 'mrkdwn', 'text' => "*Email:*\n{$purchaser_email}"],
|
||||||
|
['type' => 'mrkdwn', 'text' => "*Event:*\n{$event_title}"],
|
||||||
|
['type' => 'mrkdwn', 'text' => "*Tickets:*\n{$total_qty}"],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'section',
|
||||||
|
'fields' => [
|
||||||
|
['type' => 'mrkdwn', 'text' => "*Total:*\n{$total_formatted}"],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'context',
|
||||||
|
'elements' => [
|
||||||
|
['type' => 'mrkdwn', 'text' => "via {$gateway}"],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'actions',
|
||||||
|
'elements' => [
|
||||||
|
[
|
||||||
|
'type' => 'button',
|
||||||
|
'text' => ['type' => 'plain_text', 'text' => 'View Order'],
|
||||||
|
'url' => $admin_url,
|
||||||
|
'style' => 'primary',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
self::send($text, $blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Event notifications
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build common event Block Kit fields from an event post ID.
|
||||||
|
*
|
||||||
|
* @param int $event_id The tribe_events post ID.
|
||||||
|
* @return array{text: string, fields: array, admin_url: string, event_title: string}
|
||||||
|
*/
|
||||||
|
private static function build_event_fields(int $event_id): array {
|
||||||
|
$event = get_post($event_id);
|
||||||
|
$event_title = $event ? $event->post_title : 'N/A';
|
||||||
|
|
||||||
|
$start_date = get_post_meta($event_id, '_EventStartDate', true);
|
||||||
|
$end_date = get_post_meta($event_id, '_EventEndDate', true);
|
||||||
|
$venue_id = get_post_meta($event_id, '_EventVenueID', true);
|
||||||
|
$venue_name = $venue_id ? get_the_title((int) $venue_id) : 'N/A';
|
||||||
|
|
||||||
|
$date_display = 'N/A';
|
||||||
|
if ($start_date) {
|
||||||
|
$date_display = wp_date('M j, Y g:ia', strtotime($start_date));
|
||||||
|
if ($end_date) {
|
||||||
|
$date_display .= ' — ' . wp_date('g:ia', strtotime($end_date));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get trainer/author info
|
||||||
|
$author_id = $event ? (int) $event->post_author : 0;
|
||||||
|
$author_name = $author_id ? get_the_author_meta('display_name', $author_id) : 'N/A';
|
||||||
|
|
||||||
|
$admin_url = admin_url("post.php?post={$event_id}&action=edit");
|
||||||
|
$front_url = get_permalink($event_id) ?: $admin_url;
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
['type' => 'mrkdwn', 'text' => "*Event:*\n{$event_title}"],
|
||||||
|
['type' => 'mrkdwn', 'text' => "*Trainer:*\n{$author_name}"],
|
||||||
|
['type' => 'mrkdwn', 'text' => "*Date:*\n{$date_display}"],
|
||||||
|
['type' => 'mrkdwn', 'text' => "*Venue:*\n{$venue_name}"],
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'event_title' => $event_title,
|
||||||
|
'author_name' => $author_name,
|
||||||
|
'fields' => $fields,
|
||||||
|
'admin_url' => $admin_url,
|
||||||
|
'front_url' => $front_url,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a trainer submits an event via TEC Community Events form.
|
||||||
|
* Hooked to `hvac_tec_event_saved`.
|
||||||
|
*
|
||||||
|
* @param int $event_id The saved event post ID.
|
||||||
|
*/
|
||||||
|
public static function notify_event_submitted(int $event_id): void {
|
||||||
|
// Atomic lock: add_post_meta with $unique=true returns false if key already exists.
|
||||||
|
// Prevents duplicate sends on edits and concurrent requests.
|
||||||
|
if (!add_post_meta($event_id, '_hvac_slack_event_notified', '1', true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = self::build_event_fields($event_id);
|
||||||
|
$text = "New Event Submitted: {$data['event_title']} by {$data['author_name']}";
|
||||||
|
|
||||||
|
$blocks = [
|
||||||
|
[
|
||||||
|
'type' => 'header',
|
||||||
|
'text' => ['type' => 'plain_text', 'text' => "\xF0\x9F\x93\x9D Event Submitted by Trainer", 'emoji' => true],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'section',
|
||||||
|
'fields' => $data['fields'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'actions',
|
||||||
|
'elements' => [
|
||||||
|
[
|
||||||
|
'type' => 'button',
|
||||||
|
'text' => ['type' => 'plain_text', 'text' => 'View Event'],
|
||||||
|
'url' => $data['admin_url'],
|
||||||
|
'style' => 'primary',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
self::send($text, $blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook callback for transition_post_status on tribe_events.
|
||||||
|
* Fires when an admin publishes an event (including from draft/pending).
|
||||||
|
*
|
||||||
|
* @param string $new_status New post status.
|
||||||
|
* @param string $old_status Previous post status.
|
||||||
|
* @param WP_Post $post Post object.
|
||||||
|
*/
|
||||||
|
public static function on_event_status_change(string $new_status, string $old_status, \WP_Post $post): void {
|
||||||
|
if ($post->post_type !== 'tribe_events') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only notify when transitioning INTO publish
|
||||||
|
if ($new_status !== 'publish' || $old_status === 'publish') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomic lock: prevents double-notify from trainer submission hook or concurrent requests
|
||||||
|
if (!add_post_meta($post->ID, '_hvac_slack_event_notified', '1', true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = self::build_event_fields($post->ID);
|
||||||
|
$text = "Event Published: {$data['event_title']}";
|
||||||
|
|
||||||
|
$blocks = [
|
||||||
|
[
|
||||||
|
'type' => 'header',
|
||||||
|
'text' => ['type' => 'plain_text', 'text' => "\xE2\x9C\x85 Event Published", 'emoji' => true],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'section',
|
||||||
|
'fields' => $data['fields'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'actions',
|
||||||
|
'elements' => [
|
||||||
|
[
|
||||||
|
'type' => 'button',
|
||||||
|
'text' => ['type' => 'plain_text', 'text' => 'View Event'],
|
||||||
|
'url' => $data['front_url'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'button',
|
||||||
|
'text' => ['type' => 'plain_text', 'text' => 'Edit in WP'],
|
||||||
|
'url' => $data['admin_url'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
self::send($text, $blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Test notification (AJAX)
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler: send a test notification to verify webhook config.
|
||||||
|
* Uses blocking mode so the admin UI gets real success/failure feedback.
|
||||||
|
*/
|
||||||
|
public static function send_test_notification(): void {
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error(['message' => 'Unauthorized'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
check_ajax_referer('hvac_test_slack_webhook', 'nonce');
|
||||||
|
|
||||||
|
$env = function_exists('hvac_is_staging_environment') && hvac_is_staging_environment() ? 'Staging' : 'Production';
|
||||||
|
$site_url = home_url('/');
|
||||||
|
|
||||||
|
$text = "Test notification from HVAC Community Events ({$env})";
|
||||||
|
$blocks = [
|
||||||
|
[
|
||||||
|
'type' => 'header',
|
||||||
|
'text' => ['type' => 'plain_text', 'text' => "\xE2\x9C\x85 Slack Integration Test", 'emoji' => true],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'section',
|
||||||
|
'text' => [
|
||||||
|
'type' => 'mrkdwn',
|
||||||
|
'text' => "This is a test notification from *HVAC Community Events*.\nIf you see this, your webhook is working correctly.",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'section',
|
||||||
|
'fields' => [
|
||||||
|
['type' => 'mrkdwn', 'text' => "*Environment:*\n{$env}"],
|
||||||
|
['type' => 'mrkdwn', 'text' => "*Site:*\n{$site_url}"],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'context',
|
||||||
|
'elements' => [
|
||||||
|
['type' => 'mrkdwn', 'text' => 'Sent at ' . current_time('Y-m-d H:i:s')],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = self::send($text, $blocks, blocking: true);
|
||||||
|
|
||||||
|
if ($result === null) {
|
||||||
|
wp_send_json_error(['message' => 'No webhook URL configured.']);
|
||||||
|
} elseif ($result === false) {
|
||||||
|
wp_send_json_error(['message' => 'Slack returned an error. Check the webhook URL and try again.']);
|
||||||
|
} else {
|
||||||
|
wp_send_json_success(['message' => 'Test notification sent successfully!']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -45,8 +45,15 @@ class HVAC_Venue_Categories {
|
||||||
* Constructor
|
* Constructor
|
||||||
*/
|
*/
|
||||||
private function __construct() {
|
private function __construct() {
|
||||||
add_action('init', [$this, 'register_taxonomies'], 5);
|
// If init has already fired (we're being initialized late), register immediately
|
||||||
add_action('init', [$this, 'create_default_terms'], 10);
|
// This handles the case where the class is instantiated during 'init' priority >= 5
|
||||||
|
if (did_action('init')) {
|
||||||
|
$this->register_taxonomies();
|
||||||
|
$this->create_default_terms();
|
||||||
|
} else {
|
||||||
|
add_action('init', [$this, 'register_taxonomies'], 5);
|
||||||
|
add_action('init', [$this, 'create_default_terms'], 10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,11 @@ class HVAC_MapGeo_Integration {
|
||||||
add_action('wp_ajax_hvac_search_trainers', [$this, 'ajax_search_trainers']);
|
add_action('wp_ajax_hvac_search_trainers', [$this, 'ajax_search_trainers']);
|
||||||
add_action('wp_ajax_nopriv_hvac_search_trainers', [$this, 'ajax_search_trainers']);
|
add_action('wp_ajax_nopriv_hvac_search_trainers', [$this, 'ajax_search_trainers']);
|
||||||
|
|
||||||
// Add JavaScript to handle MapGeo marker clicks - Priority 0 to ensure interceptor runs before localization
|
// Add JavaScript to handle MapGeo marker clicks - MUST run in wp_head BEFORE shortcode renders
|
||||||
|
// The IGM plugin outputs iMapsData during shortcode execution, so interceptor must be installed first
|
||||||
|
add_action('wp_head', [$this, 'add_mapgeo_interceptor'], 1);
|
||||||
|
|
||||||
|
// Add click handlers in footer (these run after map initializes)
|
||||||
add_action('wp_footer', [$this, 'add_mapgeo_click_handlers'], 0);
|
add_action('wp_footer', [$this, 'add_mapgeo_click_handlers'], 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -400,8 +404,84 @@ class HVAC_MapGeo_Integration {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add JavaScript to handle MapGeo custom click actions
|
* Add JavaScript interceptor in wp_head BEFORE shortcode renders
|
||||||
|
* This MUST run before IGM plugin outputs iMapsData
|
||||||
*/
|
*/
|
||||||
|
public function add_mapgeo_interceptor() {
|
||||||
|
// Only add on find trainer page
|
||||||
|
if (!is_page() || get_post_field('post_name') !== 'find-a-trainer') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Strategy H (v2): Multi-stage healing for IGM coordinate corruption
|
||||||
|
// The IGM plugin corrupts longitude AFTER initial assignment, so we need multiple healing passes
|
||||||
|
(function() {
|
||||||
|
// Prevent double-installation
|
||||||
|
if (window._hvacMapInterceptorInstalled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window._hvacMapInterceptorInstalled = true;
|
||||||
|
|
||||||
|
// Healing function that can be called multiple times
|
||||||
|
window._hvacHealMapMarkers = function(source) {
|
||||||
|
if (typeof window.iMapsData === 'undefined' || !window.iMapsData) return 0;
|
||||||
|
var data = window.iMapsData;
|
||||||
|
if (!data.data || !data.data[0]) return 0;
|
||||||
|
|
||||||
|
var markerTypes = ['roundMarkers', 'iconMarkers', 'markers', 'customMarkers'];
|
||||||
|
var totalHealed = 0;
|
||||||
|
|
||||||
|
markerTypes.forEach(function(markerType) {
|
||||||
|
if (data.data[0][markerType] && Array.isArray(data.data[0][markerType])) {
|
||||||
|
data.data[0][markerType].forEach(function(m) {
|
||||||
|
// Detect corruption: latitude === longitude but backup values differ
|
||||||
|
if (m.lat && m.lng && m.latitude == m.longitude && parseFloat(m.lat) != parseFloat(m.lng)) {
|
||||||
|
m.latitude = parseFloat(m.lat);
|
||||||
|
m.longitude = parseFloat(m.lng);
|
||||||
|
totalHealed++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totalHealed > 0) {
|
||||||
|
console.log('✅ HVAC MapGeo Healer [' + source + ']: Fixed ' + totalHealed + ' corrupted coordinates.');
|
||||||
|
}
|
||||||
|
return totalHealed;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Schedule multiple healing passes to catch post-assignment corruption
|
||||||
|
var healingAttempts = 0;
|
||||||
|
var maxAttempts = 10;
|
||||||
|
var healingInterval = setInterval(function() {
|
||||||
|
healingAttempts++;
|
||||||
|
var healed = window._hvacHealMapMarkers('interval-' + healingAttempts);
|
||||||
|
|
||||||
|
// Stop after max attempts or if we've healed and no more corruption
|
||||||
|
if (healingAttempts >= maxAttempts) {
|
||||||
|
clearInterval(healingInterval);
|
||||||
|
console.log('🛡️ HVAC MapGeo Healer: Completed ' + healingAttempts + ' healing passes.');
|
||||||
|
}
|
||||||
|
}, 100); // Check every 100ms
|
||||||
|
|
||||||
|
// Also heal on DOMContentLoaded and window load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
setTimeout(function() { window._hvacHealMapMarkers('DOMContentLoaded'); }, 50);
|
||||||
|
setTimeout(function() { window._hvacHealMapMarkers('DOMContentLoaded+500'); }, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
setTimeout(function() { window._hvacHealMapMarkers('window.load'); }, 100);
|
||||||
|
setTimeout(function() { window._hvacHealMapMarkers('window.load+1000'); }, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('🛡️ HVAC MapGeo Healer installed with multi-stage healing');
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add JavaScript to handle MapGeo custom click actions
|
* Add JavaScript to handle MapGeo custom click actions
|
||||||
|
|
@ -411,43 +491,9 @@ class HVAC_MapGeo_Integration {
|
||||||
if (!is_page() || get_post_field('post_name') !== 'find-a-trainer') {
|
if (!is_page() || get_post_field('post_name') !== 'find-a-trainer') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
// Strategy H: Intercept iMapsData assignment to fix corruption
|
|
||||||
(function() {
|
|
||||||
var _data = undefined;
|
|
||||||
|
|
||||||
// Only intercept if not defined yet
|
|
||||||
if (typeof iMapsData === 'undefined') {
|
|
||||||
Object.defineProperty(window, 'iMapsData', {
|
|
||||||
get: function() { return _data; },
|
|
||||||
set: function(val) {
|
|
||||||
// Fix corruption immediately upon assignment
|
|
||||||
if(val && val.data && val.data[0]) {
|
|
||||||
// Handle roundMarkers
|
|
||||||
if (val.data[0].roundMarkers) {
|
|
||||||
var healedCount = 0;
|
|
||||||
val.data[0].roundMarkers.forEach(function(m) {
|
|
||||||
// Restore from safe lat/lng keys if corruption detected
|
|
||||||
if(m.lat && m.lng && m.latitude == m.longitude) {
|
|
||||||
m.latitude = m.lat;
|
|
||||||
m.longitude = m.lng;
|
|
||||||
healedCount++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if(healedCount > 0) {
|
|
||||||
console.log('✅ HVAC MapGeo Interceptor: Healed ' + healedCount + ' corrupted markers instantly.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_data = val;
|
|
||||||
},
|
|
||||||
configurable: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
jQuery(document).ready(function($) {
|
jQuery(document).ready(function($) {
|
||||||
// Disable console logging in production
|
// Disable console logging in production
|
||||||
var isProduction = window.location.hostname === 'upskillhvac.com';
|
var isProduction = window.location.hostname === 'upskillhvac.com';
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,21 @@ class HVAC_Find_Training_Page {
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function is_find_training_page(): bool {
|
public function is_find_training_page(): bool {
|
||||||
return is_page($this->page_slug) || is_page(get_option('hvac_find_training_page_id'));
|
if (is_page($this->page_slug)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$page_id = get_option('hvac_find_training_page_id');
|
||||||
|
return $page_id && is_page($page_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Google Maps API key is configured
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function is_api_key_configured(): bool {
|
||||||
|
return !empty($this->api_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -158,6 +172,8 @@ class HVAC_Find_Training_Page {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$api_key_configured = !empty($this->api_key);
|
||||||
|
|
||||||
// Enqueue CSS
|
// Enqueue CSS
|
||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
'hvac-find-training',
|
'hvac-find-training',
|
||||||
|
|
@ -166,8 +182,16 @@ class HVAC_Find_Training_Page {
|
||||||
HVAC_VERSION
|
HVAC_VERSION
|
||||||
);
|
);
|
||||||
|
|
||||||
// Enqueue Google Maps API with MarkerClusterer
|
// Enqueue Google reCAPTCHA for contact forms
|
||||||
if (!empty($this->api_key)) {
|
if (class_exists('HVAC_Recaptcha')) {
|
||||||
|
HVAC_Recaptcha::instance()->enqueue_script();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build script dependencies
|
||||||
|
$map_script_deps = ['jquery'];
|
||||||
|
|
||||||
|
// Enqueue Google Maps API with MarkerClusterer only if API key is configured
|
||||||
|
if ($api_key_configured) {
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
'google-maps-api',
|
'google-maps-api',
|
||||||
'https://maps.googleapis.com/maps/api/js?key=' . esc_attr($this->api_key) . '&libraries=places&callback=Function.prototype',
|
'https://maps.googleapis.com/maps/api/js?key=' . esc_attr($this->api_key) . '&libraries=places&callback=Function.prototype',
|
||||||
|
|
@ -184,13 +208,16 @@ class HVAC_Find_Training_Page {
|
||||||
'2.5.3',
|
'2.5.3',
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$map_script_deps[] = 'google-maps-api';
|
||||||
|
$map_script_deps[] = 'google-maps-markerclusterer';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enqueue main map JavaScript
|
// Enqueue main map JavaScript (always load for directory functionality)
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
'hvac-find-training-map',
|
'hvac-find-training-map',
|
||||||
HVAC_PLUGIN_URL . 'assets/js/find-training-map.js',
|
HVAC_PLUGIN_URL . 'assets/js/find-training-map.js',
|
||||||
['jquery', 'google-maps-api', 'google-maps-markerclusterer'],
|
$map_script_deps,
|
||||||
HVAC_VERSION,
|
HVAC_VERSION,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
@ -204,27 +231,38 @@ class HVAC_Find_Training_Page {
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get reCAPTCHA site key if available
|
||||||
|
$recaptcha_site_key = '';
|
||||||
|
if (class_exists('HVAC_Recaptcha')) {
|
||||||
|
$recaptcha_site_key = HVAC_Recaptcha::instance()->get_site_key();
|
||||||
|
}
|
||||||
|
|
||||||
// Localize script with data
|
// Localize script with data
|
||||||
wp_localize_script('hvac-find-training-map', 'hvacFindTraining', [
|
wp_localize_script('hvac-find-training-map', 'hvacFindTraining', [
|
||||||
'ajax_url' => admin_url('admin-ajax.php'),
|
'ajax_url' => admin_url('admin-ajax.php'),
|
||||||
'nonce' => wp_create_nonce('hvac_find_training'),
|
'nonce' => wp_create_nonce('hvac_find_training'),
|
||||||
'api_key' => !empty($this->api_key) ? 'configured' : '', // Don't expose actual key
|
'api_key_configured' => $api_key_configured,
|
||||||
'map_center' => [
|
'map_center' => [
|
||||||
'lat' => 39.8283, // US center
|
'lat' => 39.8283, // US center
|
||||||
'lng' => -98.5795
|
'lng' => -98.5795
|
||||||
],
|
],
|
||||||
'default_zoom' => 4,
|
'default_zoom' => 4,
|
||||||
'cluster_zoom' => 8,
|
'cluster_zoom' => 8,
|
||||||
|
'recaptcha_site_key' => $recaptcha_site_key,
|
||||||
'messages' => [
|
'messages' => [
|
||||||
'loading' => __('Loading...', 'hvac-community-events'),
|
'loading' => __('Loading...', 'hvac-community-events'),
|
||||||
'error' => __('An error occurred. Please try again.', 'hvac-community-events'),
|
'error' => __('An error occurred. Please try again.', 'hvac-community-events'),
|
||||||
'no_results' => __('No trainers or venues found matching your criteria.', 'hvac-community-events'),
|
'no_results' => __('No trainers or venues found matching your criteria.', 'hvac-community-events'),
|
||||||
'geolocation_error' => __('Unable to get your location. Please check your browser settings.', 'hvac-community-events'),
|
'geolocation_error' => __('Unable to get your location. Please check your browser settings.', 'hvac-community-events'),
|
||||||
'geolocation_unsupported' => __('Geolocation is not supported by your browser.', 'hvac-community-events')
|
'geolocation_unsupported' => __('Geolocation is not supported by your browser.', 'hvac-community-events'),
|
||||||
|
'api_key_missing' => __('Google Maps API key is not configured.', 'hvac-community-events'),
|
||||||
|
'captcha_required' => __('Please complete the CAPTCHA verification.', 'hvac-community-events'),
|
||||||
|
'captcha_failed' => __('CAPTCHA verification failed. Please try again.', 'hvac-community-events')
|
||||||
],
|
],
|
||||||
'marker_icons' => [
|
'marker_icons' => [
|
||||||
'trainer' => HVAC_PLUGIN_URL . 'assets/images/marker-trainer.svg',
|
'trainer' => HVAC_PLUGIN_URL . 'assets/images/marker-trainer.svg',
|
||||||
'venue' => HVAC_PLUGIN_URL . 'assets/images/marker-venue.svg'
|
'venue' => HVAC_PLUGIN_URL . 'assets/images/marker-venue.svg',
|
||||||
|
'event' => HVAC_PLUGIN_URL . 'assets/images/marker-event.svg'
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
@ -255,7 +293,7 @@ class HVAC_Find_Training_Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AJAX: Get all map data (trainers and venues)
|
* AJAX: Get all map data (trainers, venues, and events)
|
||||||
*/
|
*/
|
||||||
public function ajax_get_map_data(): void {
|
public function ajax_get_map_data(): void {
|
||||||
// Verify nonce
|
// Verify nonce
|
||||||
|
|
@ -268,12 +306,15 @@ class HVAC_Find_Training_Page {
|
||||||
|
|
||||||
$trainers = $data_provider->get_trainer_markers();
|
$trainers = $data_provider->get_trainer_markers();
|
||||||
$venues = $data_provider->get_venue_markers();
|
$venues = $data_provider->get_venue_markers();
|
||||||
|
$events = $data_provider->get_event_markers();
|
||||||
|
|
||||||
wp_send_json_success([
|
wp_send_json_success([
|
||||||
'trainers' => $trainers,
|
'trainers' => $trainers,
|
||||||
'venues' => $venues,
|
'venues' => $venues,
|
||||||
|
'events' => $events,
|
||||||
'total_trainers' => count($trainers),
|
'total_trainers' => count($trainers),
|
||||||
'total_venues' => count($venues)
|
'total_venues' => count($venues),
|
||||||
|
'total_events' => count($events)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -294,6 +335,8 @@ class HVAC_Find_Training_Page {
|
||||||
'search' => sanitize_text_field($_POST['search'] ?? ''),
|
'search' => sanitize_text_field($_POST['search'] ?? ''),
|
||||||
'show_trainers' => filter_var($_POST['show_trainers'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
'show_trainers' => filter_var($_POST['show_trainers'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
||||||
'show_venues' => filter_var($_POST['show_venues'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
'show_venues' => filter_var($_POST['show_venues'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
||||||
|
'show_events' => filter_var($_POST['show_events'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
||||||
|
'include_past' => filter_var($_POST['include_past'] ?? false, FILTER_VALIDATE_BOOLEAN),
|
||||||
'lat' => isset($_POST['lat']) ? floatval($_POST['lat']) : null,
|
'lat' => isset($_POST['lat']) ? floatval($_POST['lat']) : null,
|
||||||
'lng' => isset($_POST['lng']) ? floatval($_POST['lng']) : null,
|
'lng' => isset($_POST['lng']) ? floatval($_POST['lng']) : null,
|
||||||
'radius' => isset($_POST['radius']) ? intval($_POST['radius']) : 100 // km
|
'radius' => isset($_POST['radius']) ? intval($_POST['radius']) : 100 // km
|
||||||
|
|
@ -303,7 +346,8 @@ class HVAC_Find_Training_Page {
|
||||||
|
|
||||||
$result = [
|
$result = [
|
||||||
'trainers' => [],
|
'trainers' => [],
|
||||||
'venues' => []
|
'venues' => [],
|
||||||
|
'events' => []
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($filters['show_trainers']) {
|
if ($filters['show_trainers']) {
|
||||||
|
|
@ -314,8 +358,13 @@ class HVAC_Find_Training_Page {
|
||||||
$result['venues'] = $data_provider->get_venue_markers($filters);
|
$result['venues'] = $data_provider->get_venue_markers($filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($filters['show_events']) {
|
||||||
|
$result['events'] = $data_provider->get_event_markers($filters);
|
||||||
|
}
|
||||||
|
|
||||||
$result['total_trainers'] = count($result['trainers']);
|
$result['total_trainers'] = count($result['trainers']);
|
||||||
$result['total_venues'] = count($result['venues']);
|
$result['total_venues'] = count($result['venues']);
|
||||||
|
$result['total_events'] = count($result['events']);
|
||||||
$result['filters_applied'] = array_filter($filters, function($v) {
|
$result['filters_applied'] = array_filter($filters, function($v) {
|
||||||
return !empty($v) && $v !== true;
|
return !empty($v) && $v !== true;
|
||||||
});
|
});
|
||||||
|
|
@ -477,6 +526,11 @@ class HVAC_Find_Training_Page {
|
||||||
<div class="hvac-form-field">
|
<div class="hvac-form-field">
|
||||||
<textarea name="message" placeholder="Message" rows="3"></textarea>
|
<textarea name="message" placeholder="Message" rows="3"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="hvac-form-field hvac-recaptcha-wrapper">
|
||||||
|
<?php if (class_exists('HVAC_Recaptcha')): ?>
|
||||||
|
<?php HVAC_Recaptcha::instance()->echo_widget('trainer-contact'); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
<button type="submit" class="hvac-btn-primary">Send Message</button>
|
<button type="submit" class="hvac-btn-primary">Send Message</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,11 +69,27 @@ class HVAC_Venue_Geocoding {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load API key from secure storage
|
* Load API key from secure storage or plain option
|
||||||
|
* Uses dedicated geocoding key if available, falls back to maps key
|
||||||
*/
|
*/
|
||||||
private function load_api_key(): void {
|
private function load_api_key(): void {
|
||||||
if (class_exists('HVAC_Secure_Storage')) {
|
// First try dedicated geocoding API key (IP-restricted for server-side use)
|
||||||
$this->api_key = HVAC_Secure_Storage::get_credential('hvac_google_maps_api_key', '');
|
// Check plain option first (simpler setup)
|
||||||
|
$this->api_key = get_option('hvac_google_geocoding_api_key', '');
|
||||||
|
|
||||||
|
// Try secure storage if plain option not set
|
||||||
|
if (empty($this->api_key) && class_exists('HVAC_Secure_Storage')) {
|
||||||
|
$this->api_key = HVAC_Secure_Storage::get_credential('hvac_google_geocoding_api_key', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to maps API key if geocoding key not set
|
||||||
|
if (empty($this->api_key)) {
|
||||||
|
if (class_exists('HVAC_Secure_Storage')) {
|
||||||
|
$this->api_key = HVAC_Secure_Storage::get_credential('hvac_google_maps_api_key', '');
|
||||||
|
}
|
||||||
|
if (empty($this->api_key)) {
|
||||||
|
$this->api_key = get_option('hvac_google_maps_api_key', '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,6 +106,12 @@ class HVAC_Venue_Geocoding {
|
||||||
// Admin action for batch geocoding
|
// Admin action for batch geocoding
|
||||||
add_action('wp_ajax_hvac_batch_geocode_venues', [$this, 'ajax_batch_geocode']);
|
add_action('wp_ajax_hvac_batch_geocode_venues', [$this, 'ajax_batch_geocode']);
|
||||||
|
|
||||||
|
// Admin action for marking venues as approved labs (legacy)
|
||||||
|
add_action('wp_ajax_hvac_mark_venues_approved', [$this, 'ajax_mark_venues_approved']);
|
||||||
|
|
||||||
|
// Admin action for updating approved labs list
|
||||||
|
add_action('wp_ajax_hvac_update_approved_labs', [$this, 'ajax_update_approved_labs']);
|
||||||
|
|
||||||
// Clear venue coordinates when address changes
|
// Clear venue coordinates when address changes
|
||||||
add_action('updated_post_meta', [$this, 'on_venue_meta_update'], 10, 4);
|
add_action('updated_post_meta', [$this, 'on_venue_meta_update'], 10, 4);
|
||||||
}
|
}
|
||||||
|
|
@ -412,6 +434,158 @@ class HVAC_Venue_Geocoding {
|
||||||
wp_send_json_success($result);
|
wp_send_json_success($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for marking geocoded venues as approved training labs
|
||||||
|
*/
|
||||||
|
public function ajax_mark_venues_approved(): void {
|
||||||
|
// Check permissions
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error(['message' => 'Permission denied']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify nonce
|
||||||
|
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_mark_venues_approved')) {
|
||||||
|
wp_send_json_error(['message' => 'Invalid security token']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->mark_geocoded_venues_as_approved();
|
||||||
|
|
||||||
|
wp_send_json_success($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark all geocoded venues as approved training labs
|
||||||
|
*
|
||||||
|
* @return array Results with count of venues marked
|
||||||
|
*/
|
||||||
|
public function mark_geocoded_venues_as_approved(): array {
|
||||||
|
// Get all venues that have coordinates but are NOT already approved labs
|
||||||
|
$venues = get_posts([
|
||||||
|
'post_type' => 'tribe_venue',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'meta_query' => [
|
||||||
|
'relation' => 'OR',
|
||||||
|
[
|
||||||
|
'key' => 'venue_latitude',
|
||||||
|
'compare' => 'EXISTS'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => '_VenueLat',
|
||||||
|
'compare' => 'EXISTS'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'tax_query' => [
|
||||||
|
[
|
||||||
|
'taxonomy' => 'venue_type',
|
||||||
|
'field' => 'slug',
|
||||||
|
'terms' => 'mq-approved-lab',
|
||||||
|
'operator' => 'NOT IN'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$results = [
|
||||||
|
'marked' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
'total_approved' => 0
|
||||||
|
];
|
||||||
|
|
||||||
|
// Load venue categories class
|
||||||
|
if (!class_exists('HVAC_Venue_Categories')) {
|
||||||
|
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-venue-categories.php';
|
||||||
|
}
|
||||||
|
$venue_categories = HVAC_Venue_Categories::instance();
|
||||||
|
|
||||||
|
foreach ($venues as $venue) {
|
||||||
|
$result = $venue_categories->set_as_approved_lab($venue->ID);
|
||||||
|
if (is_wp_error($result)) {
|
||||||
|
$results['failed']++;
|
||||||
|
} else {
|
||||||
|
$results['marked']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count total approved labs
|
||||||
|
$approved_query = new WP_Query([
|
||||||
|
'post_type' => 'tribe_venue',
|
||||||
|
'posts_per_page' => 1,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'fields' => 'ids',
|
||||||
|
'tax_query' => [
|
||||||
|
[
|
||||||
|
'taxonomy' => 'venue_type',
|
||||||
|
'field' => 'slug',
|
||||||
|
'terms' => 'mq-approved-lab',
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$results['total_approved'] = $approved_query->found_posts;
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for updating approved labs list with specific venue IDs
|
||||||
|
*/
|
||||||
|
public function ajax_update_approved_labs(): void {
|
||||||
|
// Check permissions
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error(['message' => 'Permission denied']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify nonce
|
||||||
|
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_mark_venues_approved')) {
|
||||||
|
wp_send_json_error(['message' => 'Invalid security token']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get venue IDs from request (these are the ones that should be approved)
|
||||||
|
$selected_ids = isset($_POST['venue_ids']) ? array_map('absint', (array)$_POST['venue_ids']) : [];
|
||||||
|
|
||||||
|
// Load venue categories class
|
||||||
|
if (!class_exists('HVAC_Venue_Categories')) {
|
||||||
|
require_once HVAC_PLUGIN_DIR . 'includes/class-hvac-venue-categories.php';
|
||||||
|
}
|
||||||
|
$venue_categories = HVAC_Venue_Categories::instance();
|
||||||
|
|
||||||
|
// Get all venues
|
||||||
|
$all_venues = get_posts([
|
||||||
|
'post_type' => 'tribe_venue',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'fields' => 'ids'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$added = 0;
|
||||||
|
$removed = 0;
|
||||||
|
|
||||||
|
foreach ($all_venues as $venue_id) {
|
||||||
|
$is_currently_approved = has_term('mq-approved-lab', 'venue_type', $venue_id);
|
||||||
|
$should_be_approved = in_array($venue_id, $selected_ids);
|
||||||
|
|
||||||
|
if ($should_be_approved && !$is_currently_approved) {
|
||||||
|
// Add the term
|
||||||
|
wp_set_post_terms($venue_id, ['mq-approved-lab'], 'venue_type', false);
|
||||||
|
$added++;
|
||||||
|
} elseif (!$should_be_approved && $is_currently_approved) {
|
||||||
|
// Remove the term
|
||||||
|
wp_remove_object_terms($venue_id, 'mq-approved-lab', 'venue_type');
|
||||||
|
$removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success([
|
||||||
|
'approved_count' => count($selected_ids),
|
||||||
|
'added' => $added,
|
||||||
|
'removed' => $removed
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Batch geocode venues without coordinates
|
* Batch geocode venues without coordinates
|
||||||
*
|
*
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue