feat: Implement configurable Communication Schedule system

This commit implements Phase 1 of the Communication Schedule system, providing:

Core Infrastructure:
- HVAC_Communication_Scheduler: Main controller with cron integration and AJAX handlers
- HVAC_Communication_Schedule_Manager: CRUD operations and database interactions
- HVAC_Communication_Trigger_Engine: Automation logic and recipient management
- HVAC_Communication_Logger: Execution logging and performance tracking
- HVAC_Communication_Installer: Database table creation and management

Features:
- Event-based triggers (before/after event, on registration)
- Custom date scheduling with recurring options
- Flexible recipient targeting (all attendees, confirmed, custom lists)
- Template integration with placeholder replacement
- WordPress cron integration for automated execution
- Comprehensive AJAX API for schedule management
- Template quickstart options for common scenarios

UI Components:
- Communication Schedules page with full management interface
- Form-based schedule creation with validation
- Schedule listing with filtering and status management
- Modal recipient preview functionality
- Pre-configured schedule templates for quick setup

Database Design:
- hvac_communication_schedules: Schedule configurations
- hvac_communication_logs: Execution history and statistics
- hvac_event_communication_tracking: Individual email tracking

The system integrates with existing email templates and provides a foundation
for automated communication workflows for HVAC trainers.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
bengizmo 2025-06-14 00:07:30 -03:00
parent 83f9285926
commit a0d47b3b3e
15 changed files with 4629 additions and 2 deletions

View file

@ -0,0 +1,556 @@
import { test, expect } from './fixtures/auth';
import { CommonActions } from './utils/common-actions';
/**
* Communication Templates Validation E2E Tests
*
* Comprehensive tests for CRUD operations, AJAX functionality,
* and template management system
*/
test.describe('Communication Templates Full Validation', () => {
test('Navigate to templates page and verify scripts load correctly', async ({ authenticatedPage: page }) => {
test.setTimeout(30000);
const actions = new CommonActions(page);
// Monitor console for JavaScript errors
const jsErrors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
jsErrors.push(msg.text());
}
});
// Navigate to communication templates page (critical for script loading)
await actions.navigateAndWait('/communication-templates/');
await actions.screenshot('templates-page-initial-load');
// Verify page loaded correctly
await expect(page.locator('h1')).toContainText('Communication Templates');
await expect(page.locator('.hvac-templates-wrapper')).toBeVisible();
// Check that hvacTemplates object is initialized with ajaxUrl
const hvacTemplatesConfig = await page.evaluate(() => {
if (typeof window.hvacTemplates !== 'undefined') {
return {
exists: true,
hasAjaxUrl: !!window.hvacTemplates.ajaxUrl,
ajaxUrl: window.hvacTemplates.ajaxUrl
};
}
return { exists: false };
});
expect(hvacTemplatesConfig.exists).toBe(true);
expect(hvacTemplatesConfig.hasAjaxUrl).toBe(true);
console.log(`✓ hvacTemplates object loaded with AJAX URL: ${hvacTemplatesConfig.ajaxUrl}`);
// Verify no critical JavaScript errors
const criticalErrors = jsErrors.filter(error =>
error.includes('hvacTemplates') ||
error.includes('communication-templates') ||
error.includes('Uncaught')
);
expect(criticalErrors.length).toBe(0);
});
test('Create new template - Full CRUD operation', async ({ authenticatedPage: page }) => {
test.setTimeout(45000);
const actions = new CommonActions(page);
// Navigate to templates page first (required for scripts)
await actions.navigateAndWait('/communication-templates/');
await actions.screenshot('templates-create-start');
// Generate unique test data
const testData = actions.generateTestData('Template');
const templateData = {
title: testData.title,
content: `Dear {attendee_name},\n\nThank you for registering for {event_title}.\n\nEvent Details:\n- Date: {event_date}\n- Time: {event_time}\n- Location: {venue_name}\n\nBest regards,\n{trainer_name}`,
category: 'registration'
};
// Check if we need to install default templates first
const gettingStarted = page.locator('.hvac-getting-started');
if (await gettingStarted.isVisible()) {
const installButton = page.locator('a:has-text("Install Default Templates")');
if (await installButton.isVisible()) {
await installButton.click();
await actions.waitForComplexAjax();
await page.waitForTimeout(2000); // Wait for templates to load
await actions.screenshot('default-templates-installed');
}
}
// Click create new template button
const createButton = page.locator('button:has-text("Create New Template")');
await expect(createButton).toBeVisible();
await createButton.click();
await page.waitForTimeout(1000);
await actions.screenshot('template-form-opened');
// Fill in template form
await page.fill('#hvac_template_title', templateData.title);
// Try to fill content (handle TinyMCE or textarea)
const contentField = page.locator('#hvac_template_content');
if (await contentField.isVisible()) {
await contentField.fill(templateData.content);
} else {
// Try TinyMCE
await actions.fillTinyMCE('#hvac_template_content', templateData.content);
}
// Select category
await page.selectOption('#hvac_template_category', templateData.category);
await actions.screenshot('template-form-filled');
// Test placeholder insertion
const placeholderItems = page.locator('.hvac-placeholder-item');
if (await placeholderItems.count() > 0) {
// Click a placeholder to test insertion
await placeholderItems.filter({ hasText: '{venue_address}' }).click();
await page.waitForTimeout(500);
console.log('✓ Placeholder insertion tested');
}
// Save the template
const saveButton = page.locator('button:has-text("Save Template")');
await expect(saveButton).toBeVisible();
await saveButton.click();
// Wait for AJAX save operation
await actions.waitForComplexAjax();
await actions.screenshot('template-saved');
// Verify template was created
await page.waitForTimeout(2000); // Allow UI to update
const templateCard = page.locator('.hvac-template-card').filter({ hasText: testData.title });
await expect(templateCard).toBeVisible();
console.log(`✓ Template created successfully: ${testData.title}`);
return testData; // Return for use in other tests
});
test('Edit existing template', async ({ authenticatedPage: page }) => {
test.setTimeout(45000);
const actions = new CommonActions(page);
// Navigate to templates page
await actions.navigateAndWait('/communication-templates/');
await actions.screenshot('templates-edit-start');
// Find a template to edit
const templateCards = page.locator('.hvac-template-card');
const cardCount = await templateCards.count();
if (cardCount > 0) {
// Click edit on the first template
const firstCard = templateCards.first();
const editButton = firstCard.locator('button:has-text("Edit")');
if (await editButton.isVisible()) {
// Get original title for comparison
const originalTitle = await firstCard.locator('.hvac-template-card-title').textContent();
await editButton.click();
await page.waitForTimeout(1000);
await actions.screenshot('template-edit-form-opened');
// Modify the template
const titleField = page.locator('#hvac_template_title');
await expect(titleField).toBeVisible();
const updatedTitle = `Updated - ${originalTitle} - ${Date.now()}`;
await titleField.fill(updatedTitle);
// Update content
const contentField = page.locator('#hvac_template_content');
if (await contentField.isVisible()) {
const currentContent = await contentField.inputValue();
await contentField.fill(currentContent + '\n\nUpdated on: ' + new Date().toISOString());
}
await actions.screenshot('template-edit-form-modified');
// Save changes
const saveButton = page.locator('button:has-text("Save Template")');
await saveButton.click();
await actions.waitForComplexAjax();
await actions.screenshot('template-edit-saved');
// Verify update
await page.waitForTimeout(2000);
const updatedCard = page.locator('.hvac-template-card').filter({ hasText: updatedTitle });
await expect(updatedCard).toBeVisible();
console.log('✓ Template edited successfully');
} else {
console.log('⚠ No edit button found on template cards');
}
} else {
console.log('⚠ No templates available to edit');
}
});
test('Delete template', async ({ authenticatedPage: page }) => {
test.setTimeout(45000);
const actions = new CommonActions(page);
// Navigate to templates page
await actions.navigateAndWait('/communication-templates/');
await actions.screenshot('templates-delete-start');
// Find a template to delete (preferably a test template)
const templateCards = page.locator('.hvac-template-card');
const testTemplateCard = templateCards.filter({ hasText: /Template \d+/ }).first();
if (await testTemplateCard.isVisible()) {
const templateTitle = await testTemplateCard.locator('.hvac-template-card-title').textContent();
const deleteButton = testTemplateCard.locator('button:has-text("Delete")');
if (await deleteButton.isVisible()) {
// Set up dialog handler for confirmation
page.once('dialog', async dialog => {
console.log(`Dialog message: ${dialog.message()}`);
await dialog.accept();
});
await deleteButton.click();
await actions.waitForComplexAjax();
await actions.screenshot('template-deleted');
// Verify template was removed
await page.waitForTimeout(2000);
const deletedCard = page.locator('.hvac-template-card').filter({ hasText: templateTitle });
await expect(deletedCard).not.toBeVisible();
console.log(`✓ Template deleted successfully: ${templateTitle}`);
} else {
console.log('⚠ No delete button found on test template');
}
} else {
console.log('⚠ No test templates available to delete');
}
});
test('Load saved templates in email attendees page', async ({ authenticatedPage: page }) => {
test.setTimeout(45000);
const actions = new CommonActions(page);
// First navigate to dashboard to find an event
await actions.navigateAndWait('/hvac-dashboard/');
// Look for events with email functionality
const emailLinks = page.locator('a[href*="email-attendees"]');
const linkCount = await emailLinks.count();
if (linkCount > 0) {
// Navigate to email attendees page
await emailLinks.first().click();
await page.waitForLoadState('networkidle');
await actions.screenshot('email-attendees-page');
// Check for template widget
const templateToggle = page.locator('.hvac-template-toggle');
if (await templateToggle.isVisible()) {
await templateToggle.click();
await page.waitForTimeout(1000);
await actions.screenshot('template-widget-opened');
// Check if templates are loaded
const templateSelect = page.locator('#hvac_template_select, select[name="template_select"]');
if (await templateSelect.isVisible()) {
const options = await templateSelect.locator('option').count();
expect(options).toBeGreaterThan(1); // Should have more than just default option
// Select a template
await templateSelect.selectOption({ index: 1 });
await page.waitForTimeout(1000);
// Click load template button
const loadButton = page.locator('button:has-text("Load Template")');
if (await loadButton.isVisible()) {
await loadButton.click();
await actions.waitForAjax();
await actions.screenshot('template-loaded');
// Verify content was loaded into email form
const emailContent = page.locator('#email_message, textarea[name="email_message"]');
const content = await emailContent.inputValue();
expect(content.length).toBeGreaterThan(0);
console.log('✓ Template loaded successfully in email form');
}
} else {
console.log('⚠ Template select dropdown not found');
}
} else {
console.log('⚠ Template widget not found on email attendees page');
}
} else {
console.log('⚠ No events with email functionality found');
}
});
test('Test placeholder functionality in templates', async ({ authenticatedPage: page }) => {
test.setTimeout(30000);
const actions = new CommonActions(page);
// Navigate to templates page
await actions.navigateAndWait('/communication-templates/');
await actions.screenshot('placeholder-test-start');
// Open create template form
const createButton = page.locator('button:has-text("Create New Template")');
if (await createButton.isVisible()) {
await createButton.click();
await page.waitForTimeout(1000);
// Check placeholder helper is visible
const placeholderHelper = page.locator('.hvac-placeholder-helper');
await expect(placeholderHelper).toBeVisible();
// Test all available placeholders
const placeholders = [
'{attendee_name}',
'{attendee_email}',
'{event_title}',
'{event_date}',
'{event_time}',
'{venue_name}',
'{venue_address}',
'{trainer_name}',
'{trainer_email}',
'{certificate_link}'
];
// Verify placeholders are displayed
for (const placeholder of placeholders) {
const placeholderItem = page.locator('.hvac-placeholder-item').filter({ hasText: placeholder });
if (await placeholderItem.isVisible()) {
console.log(`✓ Placeholder ${placeholder} is available`);
}
}
// Test clicking placeholders to insert
const contentField = page.locator('#hvac_template_content');
await contentField.clear();
// Click multiple placeholders
await page.locator('.hvac-placeholder-item').filter({ hasText: '{attendee_name}' }).click();
await page.waitForTimeout(300);
await page.locator('.hvac-placeholder-item').filter({ hasText: '{event_title}' }).click();
await page.waitForTimeout(300);
// Verify placeholders were inserted
const content = await contentField.inputValue();
expect(content).toContain('{attendee_name}');
expect(content).toContain('{event_title}');
await actions.screenshot('placeholders-inserted');
console.log('✓ Placeholder insertion functionality working');
}
});
test('Verify AJAX operations work correctly', async ({ authenticatedPage: page }) => {
test.setTimeout(45000);
const actions = new CommonActions(page);
// Monitor network requests
const ajaxRequests: string[] = [];
page.on('request', request => {
if (request.url().includes('admin-ajax.php')) {
ajaxRequests.push(request.postData() || 'GET request');
}
});
// Navigate to templates page
await actions.navigateAndWait('/communication-templates/');
// Test various AJAX operations
// 1. Test loading templates (should happen on page load)
await page.waitForTimeout(2000);
const loadRequests = ajaxRequests.filter(req => req.includes('hvac_get_templates'));
expect(loadRequests.length).toBeGreaterThan(0);
console.log(`✓ Template loading AJAX requests: ${loadRequests.length}`);
// 2. Test creating a template via AJAX
const createButton = page.locator('button:has-text("Create New Template")');
if (await createButton.isVisible()) {
await createButton.click();
await page.waitForTimeout(1000);
// Fill minimal data
await page.fill('#hvac_template_title', `AJAX Test ${Date.now()}`);
await page.fill('#hvac_template_content', 'Testing AJAX save functionality');
await page.selectOption('#hvac_template_category', 'general');
// Clear previous requests
ajaxRequests.length = 0;
// Save template
const saveButton = page.locator('button:has-text("Save Template")');
await saveButton.click();
await actions.waitForComplexAjax();
// Check for save AJAX request
const saveRequests = ajaxRequests.filter(req => req.includes('hvac_save_template'));
expect(saveRequests.length).toBeGreaterThan(0);
console.log('✓ Template save AJAX request successful');
}
// 3. Test loading template in email form (if available)
await actions.navigateAndWait('/hvac-dashboard/');
const emailLink = page.locator('a[href*="email-attendees"]').first();
if (await emailLink.isVisible()) {
await emailLink.click();
await page.waitForLoadState('networkidle');
const templateToggle = page.locator('.hvac-template-toggle');
if (await templateToggle.isVisible()) {
await templateToggle.click();
await page.waitForTimeout(1000);
// Clear requests
ajaxRequests.length = 0;
// Load a template
const templateSelect = page.locator('#hvac_template_select, select[name="template_select"]');
if (await templateSelect.isVisible() && await templateSelect.locator('option').count() > 1) {
await templateSelect.selectOption({ index: 1 });
const loadButton = page.locator('button:has-text("Load Template")');
if (await loadButton.isVisible()) {
await loadButton.click();
await actions.waitForAjax();
// Check for load template AJAX request
const loadTemplateRequests = ajaxRequests.filter(req =>
req.includes('hvac_load_template') || req.includes('hvac_get_template')
);
expect(loadTemplateRequests.length).toBeGreaterThan(0);
console.log('✓ Template load in email form AJAX request successful');
}
}
}
}
await actions.screenshot('ajax-operations-complete');
console.log(`✓ Total AJAX requests captured: ${ajaxRequests.length}`);
});
test('Verify template categories work correctly', async ({ authenticatedPage: page }) => {
test.setTimeout(30000);
const actions = new CommonActions(page);
// Navigate to templates page
await actions.navigateAndWait('/communication-templates/');
await actions.screenshot('categories-test-start');
// Check if category filter exists
const categoryFilter = page.locator('.hvac-category-filter, select[name="category_filter"]');
if (await categoryFilter.isVisible()) {
// Get available categories
const options = await categoryFilter.locator('option').allTextContents();
console.log(`Available categories: ${options.join(', ')}`);
// Test filtering by each category
for (let i = 1; i < Math.min(options.length, 4); i++) { // Test up to 3 categories
await categoryFilter.selectOption({ index: i });
await actions.waitForAjax();
await page.waitForTimeout(1000);
// Check if templates are filtered
const visibleCards = await page.locator('.hvac-template-card:visible').count();
console.log(`✓ Category "${options[i]}" shows ${visibleCards} templates`);
await actions.screenshot(`category-filter-${options[i].toLowerCase()}`);
}
// Reset to all categories
await categoryFilter.selectOption({ index: 0 });
await actions.waitForAjax();
} else {
console.log('⚠ Category filter not found - may not have enough templates');
}
});
test('Full end-to-end template workflow', async ({ authenticatedPage: page }) => {
test.setTimeout(60000);
const actions = new CommonActions(page);
console.log('Starting full end-to-end template workflow test...');
// Step 1: Navigate to templates page
await actions.navigateAndWait('/communication-templates/');
await actions.screenshot('e2e-workflow-start');
// Step 2: Create a new template
const testData = actions.generateTestData('E2E Template');
const createButton = page.locator('button:has-text("Create New Template")');
if (await createButton.isVisible()) {
await createButton.click();
await page.waitForTimeout(1000);
// Fill template with placeholders
await page.fill('#hvac_template_title', testData.title);
await page.fill('#hvac_template_content',
`Hello {attendee_name},\n\nThis is a test template for {event_title}.\n\nSee you at {venue_name}!\n\n{trainer_name}`
);
await page.selectOption('#hvac_template_category', 'reminder');
// Save template
await page.locator('button:has-text("Save Template")').click();
await actions.waitForComplexAjax();
console.log('✓ Template created');
}
// Step 3: Navigate to email attendees and use the template
await actions.navigateAndWait('/hvac-dashboard/');
const emailLink = page.locator('a[href*="email-attendees"]').first();
if (await emailLink.isVisible()) {
await emailLink.click();
await page.waitForLoadState('networkidle');
// Open template widget
const templateToggle = page.locator('.hvac-template-toggle');
if (await templateToggle.isVisible()) {
await templateToggle.click();
await page.waitForTimeout(1000);
// Select our created template
const templateSelect = page.locator('#hvac_template_select, select[name="template_select"]');
if (await templateSelect.isVisible()) {
// Find our template in the dropdown
const optionWithText = page.locator(`option:has-text("${testData.title}")`);
if (await optionWithText.count() > 0) {
const optionValue = await optionWithText.getAttribute('value');
await templateSelect.selectOption(optionValue);
// Load the template
await page.locator('button:has-text("Load Template")').click();
await actions.waitForAjax();
// Verify content loaded
const emailContent = page.locator('#email_message, textarea[name="email_message"]');
const loadedContent = await emailContent.inputValue();
expect(loadedContent).toContain('{attendee_name}');
expect(loadedContent).toContain('{event_title}');
console.log('✓ Template loaded and ready for use');
await actions.screenshot('e2e-workflow-complete');
}
}
}
}
console.log('✓ Full end-to-end workflow completed successfully');
});
});

View file

@ -0,0 +1,68 @@
import { test, expect } from './fixtures/auth';
import { CommonActions } from './utils/common-actions';
test('Debug button click handler', async ({ authenticatedPage: page }) => {
test.setTimeout(30000);
const actions = new CommonActions(page);
// Navigate to templates page
await actions.navigateAndWait('/communication-templates/');
// Wait for scripts
await page.waitForFunction(() => typeof HVACTemplates !== 'undefined');
// Check what happens when we click the button
const buttonTest = await page.evaluate(() => {
const button = document.querySelector('button:has-text("Create New Template")');
if (!button) return { error: 'Button not found' };
const onclick = button.getAttribute('onclick');
return {
buttonFound: true,
onclickAttribute: onclick,
hasOnclickFunction: typeof HVACTemplates.createNewTemplate === 'function'
};
});
console.log('Button investigation:', buttonTest);
// Try to call the function manually and see what happens
const manualCall = await page.evaluate(() => {
try {
console.log('Calling HVACTemplates.createNewTemplate...');
HVACTemplates.createNewTemplate();
const overlay = document.getElementById('template-form-overlay');
return {
success: true,
overlayExists: !!overlay,
display: overlay ? overlay.style.display : 'no overlay'
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
console.log('Manual function call result:', manualCall);
// Monitor console messages during button click
const consoleMessages: string[] = [];
page.on('console', (msg) => {
consoleMessages.push(`[${msg.type()}] ${msg.text()}`);
});
// Click the button and see what happens
await page.locator('button:has-text("Create New Template")').click();
await page.waitForTimeout(2000);
console.log('Console messages after button click:', consoleMessages);
// Check if modal is visible
const modalVisible = await page.locator('#template-form-overlay').isVisible();
console.log('Modal visible after button click:', modalVisible);
await actions.screenshot('debug-button-click-result');
});

View file

@ -0,0 +1,54 @@
import { test, expect } from './fixtures/auth';
import { CommonActions } from './utils/common-actions';
test('Debug button click simple', async ({ authenticatedPage: page }) => {
test.setTimeout(30000);
const actions = new CommonActions(page);
// Navigate to templates page
await actions.navigateAndWait('/communication-templates/');
// Wait for scripts
await page.waitForFunction(() => typeof HVACTemplates !== 'undefined');
// Check what happens when we click the button
const buttonTest = await page.evaluate(() => {
const buttons = document.querySelectorAll('button');
const createButton = Array.from(buttons).find(btn => btn.textContent?.includes('Create New Template'));
if (!createButton) return { error: 'Button not found' };
const onclick = createButton.getAttribute('onclick');
return {
buttonFound: true,
onclickAttribute: onclick,
hasOnclickFunction: typeof HVACTemplates.createNewTemplate === 'function',
buttonText: createButton.textContent
};
});
console.log('Button investigation:', buttonTest);
// Try calling the onclick directly
const onclickResult = await page.evaluate(() => {
try {
// Try to execute the onclick directly
eval('HVACTemplates.createNewTemplate()');
const overlay = document.getElementById('template-form-overlay');
return {
success: true,
display: overlay ? overlay.style.display : 'no overlay'
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
console.log('Onclick execution result:', onclickResult);
await actions.screenshot('debug-button-simple-result');
});

View file

@ -0,0 +1,66 @@
import { test, expect } from './fixtures/auth';
import { CommonActions } from './utils/common-actions';
test('Debug modal visibility', async ({ authenticatedPage: page }) => {
test.setTimeout(30000);
const actions = new CommonActions(page);
// Navigate to templates page
await actions.navigateAndWait('/communication-templates/');
// Wait for scripts
await page.waitForFunction(() => typeof HVACTemplates !== 'undefined');
// Check initial modal state
const modalInitialState = await page.evaluate(() => {
const overlay = document.getElementById('template-form-overlay');
return {
exists: !!overlay,
display: overlay ? getComputedStyle(overlay).display : 'not found',
visibility: overlay ? getComputedStyle(overlay).visibility : 'not found'
};
});
console.log('Modal initial state:', modalInitialState);
// Click create button
const createButton = page.locator('button:has-text("Create New Template")');
await expect(createButton).toBeVisible();
console.log('Clicking create button...');
await createButton.click();
// Wait a moment
await page.waitForTimeout(2000);
// Check modal state after click
const modalAfterClick = await page.evaluate(() => {
const overlay = document.getElementById('template-form-overlay');
return {
exists: !!overlay,
display: overlay ? getComputedStyle(overlay).display : 'not found',
visibility: overlay ? getComputedStyle(overlay).visibility : 'not found'
};
});
console.log('Modal after click:', modalAfterClick);
// Try calling the function directly
const directCall = await page.evaluate(() => {
if (typeof HVACTemplates !== 'undefined' && HVACTemplates.createNewTemplate) {
HVACTemplates.createNewTemplate();
const overlay = document.getElementById('template-form-overlay');
return {
functionExists: true,
display: overlay ? getComputedStyle(overlay).display : 'not found'
};
}
return { functionExists: false };
});
console.log('Direct function call result:', directCall);
// Take a screenshot
await actions.screenshot('modal-debug-final');
});

View file

@ -0,0 +1,38 @@
import { test, expect } from './fixtures/auth';
import { CommonActions } from './utils/common-actions';
test('Debug loaded scripts', async ({ authenticatedPage: page }) => {
test.setTimeout(30000);
const actions = new CommonActions(page);
// Navigate to templates page
await actions.navigateAndWait('/communication-templates/');
// Wait for scripts
await page.waitForFunction(() => typeof HVACTemplates !== 'undefined');
// Check what scripts are loaded
const scriptInfo = await page.evaluate(() => {
const scripts = Array.from(document.querySelectorAll('script[src]'));
const hvacScripts = scripts.filter(script => script.src.includes('hvac') || script.src.includes('communication'));
return {
totalScripts: scripts.length,
hvacScripts: hvacScripts.map(script => script.src),
hasHVACTemplates: typeof HVACTemplates !== 'undefined',
createNewTemplateFunction: HVACTemplates ? HVACTemplates.createNewTemplate.toString() : 'not found'
};
});
console.log('Script analysis:', JSON.stringify(scriptInfo, null, 2));
// Check if external JS file is also loading and overriding
const jsFile = await page.evaluate(() => {
const scripts = Array.from(document.querySelectorAll('script[src]'));
return scripts.find(script => script.src.includes('communication-templates.js'))?.src;
});
console.log('External JS file found:', jsFile);
await actions.screenshot('scripts-debug');
});

View file

@ -0,0 +1,332 @@
import { test, expect } from './fixtures/auth';
import { CommonActions } from './utils/common-actions';
/**
* Debug Tests for Communication Templates
*
* Comprehensive debugging to identify why features aren't working
*/
test.describe('Debug Communication Templates', () => {
test('Debug page source and JavaScript loading', async ({ authenticatedPage: page }) => {
test.setTimeout(30000);
const actions = new CommonActions(page);
// Capture all console messages
const consoleMessages: { type: string, text: string }[] = [];
page.on('console', (msg) => {
consoleMessages.push({ type: msg.type(), text: msg.text() });
});
// Navigate to templates page
await actions.navigateAndWait('/communication-templates/');
await actions.screenshot('debug-templates-page');
// Get page source
const pageSource = await page.content();
// Check if shortcode was processed
const hasShortcode = pageSource.includes('[hvac_communication_templates]');
const hasTemplateWrapper = pageSource.includes('hvac-templates-wrapper');
console.log('=== Page Debug Info ===');
console.log('Raw shortcode visible:', hasShortcode);
console.log('Template wrapper present:', hasTemplateWrapper);
// Check for PHP errors in source
const phpErrors = pageSource.match(/Fatal error:|Warning:|Notice:|Parse error:/gi);
if (phpErrors) {
console.log('PHP Errors found:', phpErrors);
}
// Check if CSS is loaded
const cssLoaded = await page.evaluate(() => {
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
return links.some(link => link.href.includes('communication-templates.css'));
});
console.log('CSS loaded:', cssLoaded);
// Check if JavaScript is loaded
const jsLoaded = await page.evaluate(() => {
const scripts = Array.from(document.querySelectorAll('script'));
return scripts.some(script => script.src && script.src.includes('communication-templates.js'));
});
console.log('JavaScript loaded:', jsLoaded);
// Check JavaScript objects
const jsObjects = await page.evaluate(() => {
return {
jQuery: typeof jQuery !== 'undefined',
hvacTemplates: typeof hvacTemplates !== 'undefined',
HVACTemplates: typeof HVACTemplates !== 'undefined',
ajaxUrl: typeof hvacTemplates !== 'undefined' ? hvacTemplates.ajaxUrl : 'not found'
};
});
console.log('JavaScript objects:', jsObjects);
// Log all console messages
console.log('\n=== Console Messages ===');
consoleMessages.forEach(msg => {
console.log(`[${msg.type}] ${msg.text}`);
});
// Check page title to verify we're on the right page
const pageTitle = await page.title();
console.log('\nPage title:', pageTitle);
// Save page source for analysis
const fs = require('fs');
fs.writeFileSync('test-results/debug-page-source.html', pageSource);
console.log('\nPage source saved to test-results/debug-page-source.html');
});
test('Test AJAX endpoints directly', async ({ authenticatedPage: page }) => {
test.setTimeout(20000);
// Test get templates endpoint
const getTemplatesResponse = await page.evaluate(async () => {
if (typeof jQuery === 'undefined') return { error: 'jQuery not loaded' };
if (typeof hvacTemplates === 'undefined') return { error: 'hvacTemplates not defined' };
return new Promise((resolve) => {
jQuery.ajax({
url: hvacTemplates.ajaxUrl,
type: 'POST',
data: {
action: 'hvac_get_templates',
nonce: hvacTemplates.nonce
},
success: function(response) {
resolve({ success: true, data: response });
},
error: function(xhr, status, error) {
resolve({ success: false, error: error, status: xhr.status });
}
});
});
});
console.log('Get Templates Response:', JSON.stringify(getTemplatesResponse, null, 2));
// Test save template endpoint
const saveTemplateResponse = await page.evaluate(async () => {
if (typeof jQuery === 'undefined') return { error: 'jQuery not loaded' };
if (typeof hvacTemplates === 'undefined') return { error: 'hvacTemplates not defined' };
return new Promise((resolve) => {
jQuery.ajax({
url: hvacTemplates.ajaxUrl,
type: 'POST',
data: {
action: 'hvac_save_template',
nonce: hvacTemplates.nonce,
title: 'Test Template',
content: 'This is a test template content with {attendee_name} placeholder.',
category: 'general',
description: 'Test template created by E2E test'
},
success: function(response) {
resolve({ success: true, data: response });
},
error: function(xhr, status, error) {
resolve({ success: false, error: error, status: xhr.status, responseText: xhr.responseText });
}
});
});
});
console.log('Save Template Response:', JSON.stringify(saveTemplateResponse, null, 2));
});
test('Check template post type registration', async ({ authenticatedPage: page }) => {
test.setTimeout(20000);
// Check if post type exists via REST API
const postTypeExists = await page.evaluate(async () => {
try {
const response = await fetch('/wp-json/wp/v2/types/hvac_email_template');
return {
exists: response.ok,
status: response.status,
data: response.ok ? await response.json() : null
};
} catch (error) {
return { exists: false, error: error.message };
}
});
console.log('Post Type Check:', JSON.stringify(postTypeExists, null, 2));
// Try to access templates via REST API
const templatesViaRest = await page.evaluate(async () => {
try {
const response = await fetch('/wp-json/wp/v2/hvac_email_template');
return {
success: response.ok,
status: response.status,
data: response.ok ? await response.json() : await response.text()
};
} catch (error) {
return { success: false, error: error.message };
}
});
console.log('Templates via REST:', JSON.stringify(templatesViaRest, null, 2));
});
test('Test template widget in email attendees page', async ({ authenticatedPage: page }) => {
test.setTimeout(30000);
const actions = new CommonActions(page);
// First, create a test event with attendees
await actions.navigateAndWait('/manage-event/');
// Fill in basic event details
const eventTitle = `Test Event ${Date.now()}`;
await page.fill('#event_title', eventTitle);
// Fill description
const descFrame = page.frameLocator('iframe[id*="_ifr"]').first();
await descFrame.locator('body').fill('Test event for template validation');
// Set date/time (tomorrow)
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const dateStr = tomorrow.toISOString().split('T')[0];
await page.fill('input[name="EventStartDate"]', dateStr);
await page.fill('input[name="EventStartTime"]', '10:00');
await page.fill('input[name="EventEndDate"]', dateStr);
await page.fill('input[name="EventEndTime"]', '12:00');
// Submit
await page.click('button[type="submit"]');
await page.waitForLoadState('networkidle');
// Now navigate to dashboard to find the event
await actions.navigateAndWait('/hvac-dashboard/');
// Look for email attendees link for our event
const emailLink = page.locator(`a[href*="email-attendees"]:has-text("${eventTitle}")`).first();
if (await emailLink.isVisible()) {
await emailLink.click();
await page.waitForLoadState('networkidle');
await actions.screenshot('email-attendees-with-widget');
// Check for template manager elements
const widgetExists = await page.locator('.hvac-template-manager').count() > 0;
const toggleExists = await page.locator('.hvac-template-toggle').count() > 0;
console.log('Template widget exists:', widgetExists);
console.log('Template toggle exists:', toggleExists);
// Check if scripts are loaded on this page
const scriptsOnEmailPage = await page.evaluate(() => {
return {
jQuery: typeof jQuery !== 'undefined',
hvacTemplates: typeof hvacTemplates !== 'undefined',
HVACTemplates: typeof HVACTemplates !== 'undefined'
};
});
console.log('Scripts on email page:', scriptsOnEmailPage);
} else {
console.log('Could not find email attendees link for test event');
}
});
test('Test user capabilities and permissions', async ({ authenticatedPage: page }) => {
test.setTimeout(20000);
// Check current user capabilities
const userInfo = await page.evaluate(async () => {
try {
const response = await fetch('/wp-json/wp/v2/users/me', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
return {
id: data.id,
name: data.name,
roles: data.roles,
capabilities: data.capabilities
};
}
return { error: 'Failed to get user info', status: response.status };
} catch (error) {
return { error: error.message };
}
});
console.log('Current User Info:', JSON.stringify(userInfo, null, 2));
// Check if user can create posts
const canCreatePosts = await page.evaluate(() => {
// This would need to be exposed by WordPress
return typeof wp !== 'undefined' && wp.data ?
wp.data.select('core').canUser('create', 'posts') :
'WordPress data not available';
});
console.log('Can create posts:', canCreatePosts);
});
test('Manually test template creation flow', async ({ authenticatedPage: page }) => {
test.setTimeout(30000);
const actions = new CommonActions(page);
// Navigate to templates page
await actions.navigateAndWait('/communication-templates/');
// Try clicking the create button if it exists
const createButton = page.locator('button:has-text("Create New Template")');
if (await createButton.isVisible()) {
await createButton.click();
await page.waitForTimeout(2000);
await actions.screenshot('after-create-click');
// Check what happened
const modalVisible = await page.locator('.hvac-template-form-overlay').isVisible();
const formVisible = await page.locator('.hvac-template-form').isVisible();
console.log('Modal visible after click:', modalVisible);
console.log('Form visible after click:', formVisible);
// Try to fill the form if visible
if (modalVisible || formVisible) {
const titleInput = page.locator('#hvac_template_title');
if (await titleInput.isVisible()) {
await titleInput.fill('E2E Test Template');
await page.locator('#hvac_template_content').fill('Test content with {attendee_name}');
await page.selectOption('#hvac_template_category', 'general');
await actions.screenshot('form-filled');
// Try to save
const saveButton = page.locator('.hvac-template-form-save');
if (await saveButton.isVisible()) {
await saveButton.click();
await page.waitForTimeout(3000);
await actions.screenshot('after-save-attempt');
// Check for success/error messages
const messages = await page.locator('.hvac-template-message').allTextContents();
console.log('Messages after save:', messages);
}
}
}
} else {
console.log('Create button not found - checking page structure');
// Get all visible text on page
const visibleText = await page.locator('body').innerText();
console.log('Page text preview:', visibleText.substring(0, 500));
}
});
});

View file

@ -0,0 +1,51 @@
import { test, expect } from './fixtures/auth';
import { CommonActions } from './utils/common-actions';
test('Simple modal test', async ({ authenticatedPage: page }) => {
test.setTimeout(30000);
const actions = new CommonActions(page);
// Navigate to templates page
await actions.navigateAndWait('/communication-templates/');
// Wait for scripts
await page.waitForFunction(() => typeof HVACTemplates !== 'undefined');
// Try to show modal directly with JavaScript
const modalResult = await page.evaluate(() => {
// Force show modal
const overlay = document.getElementById('template-form-overlay');
if (overlay) {
overlay.style.display = 'block';
overlay.style.visibility = 'visible';
overlay.style.opacity = '1';
return {
success: true,
display: getComputedStyle(overlay).display,
visibility: getComputedStyle(overlay).visibility
};
}
return { success: false, error: 'Modal not found' };
});
console.log('Manual modal show result:', modalResult);
// Take screenshot
await actions.screenshot('modal-manually-shown');
// Check if it's visible now
const modalVisibility = await page.locator('#template-form-overlay').isVisible();
console.log('Modal visible after manual show:', modalVisibility);
// If it's visible, try to interact with it
if (modalVisibility) {
// Try to fill the title field
const titleField = page.locator('#hvac_template_title');
await expect(titleField).toBeVisible();
await titleField.fill('Manual Test Template');
console.log('Successfully interacted with modal form');
await actions.screenshot('modal-form-filled');
}
});

View file

@ -99,6 +99,14 @@ function hvac_ce_create_required_pages() {
'title' => 'Google Sheets Integration',
'content' => '<!-- wp:shortcode -->[hvac_google_sheets]<!-- /wp:shortcode -->',
],
'communication-templates' => [ // Add Communication Templates page
'title' => 'Communication Templates',
'content' => '<!-- wp:shortcode -->[hvac_communication_templates]<!-- /wp:shortcode -->',
],
'communication-schedules' => [ // Add Communication Schedules page
'title' => 'Communication Schedules',
'content' => '<!-- wp:shortcode -->[hvac_communication_schedules]<!-- /wp:shortcode -->',
],
// REMOVED: 'submit-event' page creation. Will link to default TEC CE page.
// 'submit-event' => [
// 'title' => 'Submit Event',
@ -253,7 +261,7 @@ function hvac_ce_enqueue_common_assets() {
'hvac-dashboard', 'community-login', 'trainer-registration', 'trainer-profile',
'manage-event', 'event-summary', 'email-attendees', 'certificate-reports',
'generate-certificates', 'certificate-fix', 'hvac-documentation', 'attendee-profile',
'master-dashboard', 'google-sheets'
'master-dashboard', 'google-sheets', 'communication-templates', 'communication-schedules'
];
// Only proceed if we're on an HVAC page

View file

@ -79,7 +79,12 @@ class HVAC_Community_Events {
'certificates/test-rewrite-rules.php', // Rewrite rules testing (temporary)
'google-sheets/class-google-sheets-auth.php', // Google Sheets authentication
'google-sheets/class-google-sheets-manager.php', // Google Sheets management
'communication/class-communication-templates.php' // Email template management
'communication/class-communication-templates.php', // Email template management
'communication/class-communication-installer.php', // Communication system database installer
'communication/class-communication-schedule-manager.php', // Communication schedule manager
'communication/class-communication-trigger-engine.php', // Communication trigger engine
'communication/class-communication-logger.php', // Communication logger
'communication/class-communication-scheduler.php' // Communication scheduler
];
// Make sure Login_Handler is loaded first for shortcode registration
$login_handler_path = HVAC_CE_PLUGIN_DIR . 'includes/community/class-login-handler.php';
@ -302,6 +307,9 @@ class HVAC_Community_Events {
if (class_exists('HVAC_Help_System')) {
HVAC_Help_System::instance();
}
// Initialize communication system
$this->init_communication_system();
}
/**
@ -389,6 +397,9 @@ class HVAC_Community_Events {
// Add communication templates shortcode
add_shortcode('hvac_communication_templates', array($this, 'render_communication_templates'));
// Add communication schedules shortcode
add_shortcode('hvac_communication_schedules', array($this, 'render_communication_schedules'));
// Removed shortcode override - let The Events Calendar Community Events handle this shortcode
// add_shortcode('tribe_community_events', array($this, 'render_tribe_community_events'));
@ -788,6 +799,25 @@ class HVAC_Community_Events {
}
}
/**
* Initialize communication system
*/
private function init_communication_system() {
// Check and install/update communication database tables
if (class_exists('HVAC_Communication_Installer')) {
HVAC_Communication_Installer::maybe_update();
}
// Initialize the communication scheduler (singleton)
if (class_exists('HVAC_Communication_Scheduler')) {
hvac_communication_scheduler();
}
if (class_exists('HVAC_Logger')) {
HVAC_Logger::info('Communication system initialized', 'Communication System');
}
}
/**
* Render communication templates content
*/
@ -811,5 +841,29 @@ class HVAC_Community_Events {
include HVAC_CE_PLUGIN_DIR . 'templates/communication/template-communication-templates.php';
return ob_get_clean();
}
/**
* Render communication schedules content
*/
public function render_communication_schedules() {
if (!is_user_logged_in()) {
return '<p>Please log in to manage communication schedules.</p>';
}
// Check if user is a trainer or has permission to manage schedules
$current_user = wp_get_current_user();
if (!in_array('hvac_trainer', $current_user->roles) && !current_user_can('edit_posts')) {
return '<p>You do not have permission to manage communication schedules.</p>';
}
// Initialize the communication scheduler class
if (!class_exists('HVAC_Communication_Scheduler')) {
require_once HVAC_CE_PLUGIN_DIR . 'includes/communication/class-communication-scheduler.php';
}
ob_start();
include HVAC_CE_PLUGIN_DIR . 'templates/communication/template-communication-schedules.php';
return ob_get_clean();
}
} // End class HVAC_Community_Events

View file

@ -0,0 +1,383 @@
<?php
/**
* HVAC Community Events - Communication Installer
*
* Handles database table creation and updates for communication system.
* Creates tables for schedules, logs, and tracking.
*
* @package HVAC_Community_Events
* @subpackage Communication
* @version 1.0.0
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class HVAC_Communication_Installer
*
* Manages database installation for communication system.
*/
class HVAC_Communication_Installer {
/**
* Database version
*/
const DB_VERSION = '1.0.0';
/**
* Install database tables
*/
public static function install() {
global $wpdb;
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
$charset_collate = $wpdb->get_charset_collate();
self::create_schedules_table( $charset_collate );
self::create_logs_table( $charset_collate );
self::create_tracking_table( $charset_collate );
// Update version option
update_option( 'hvac_communication_db_version', self::DB_VERSION );
if ( class_exists( 'HVAC_Logger' ) ) {
HVAC_Logger::info( 'Communication system database tables installed', 'Communication Installer' );
}
}
/**
* Create communication schedules table
*
* @param string $charset_collate Database charset and collation
*/
private static function create_schedules_table( $charset_collate ) {
global $wpdb;
$table_name = $wpdb->prefix . 'hvac_communication_schedules';
$sql = "CREATE TABLE {$table_name} (
schedule_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
trainer_id BIGINT(20) UNSIGNED NOT NULL,
event_id BIGINT(20) UNSIGNED DEFAULT NULL,
template_id BIGINT(20) UNSIGNED NOT NULL,
schedule_name VARCHAR(255) NOT NULL DEFAULT '',
schedule_type VARCHAR(50) NOT NULL DEFAULT 'time_based',
trigger_type VARCHAR(50) NOT NULL,
trigger_value INT(11) NOT NULL DEFAULT 0,
trigger_unit VARCHAR(20) NOT NULL DEFAULT 'days',
status VARCHAR(20) NOT NULL DEFAULT 'active',
target_audience VARCHAR(50) NOT NULL DEFAULT 'all_attendees',
custom_recipient_list TEXT DEFAULT NULL,
conditions TEXT DEFAULT NULL,
next_run DATETIME DEFAULT NULL,
last_run DATETIME DEFAULT NULL,
run_count INT(11) NOT NULL DEFAULT 0,
is_recurring TINYINT(1) NOT NULL DEFAULT 0,
recurring_interval INT(11) DEFAULT NULL,
recurring_unit VARCHAR(20) DEFAULT NULL,
max_runs INT(11) DEFAULT NULL,
created_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
modified_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (schedule_id),
KEY trainer_id (trainer_id),
KEY event_id (event_id),
KEY template_id (template_id),
KEY status (status),
KEY trigger_type (trigger_type),
KEY next_run (next_run),
KEY created_date (created_date)
) {$charset_collate};";
dbDelta( $sql );
}
/**
* Create communication logs table
*
* @param string $charset_collate Database charset and collation
*/
private static function create_logs_table( $charset_collate ) {
global $wpdb;
$table_name = $wpdb->prefix . 'hvac_communication_logs';
$sql = "CREATE TABLE {$table_name} (
log_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
schedule_id BIGINT(20) UNSIGNED NOT NULL,
recipient_email VARCHAR(255) DEFAULT NULL,
status VARCHAR(20) NOT NULL,
sent_date DATETIME NOT NULL,
recipient_count INT(11) NOT NULL DEFAULT 0,
success_count INT(11) NOT NULL DEFAULT 0,
error_count INT(11) NOT NULL DEFAULT 0,
execution_time DECIMAL(8,4) NOT NULL DEFAULT 0.0000,
details TEXT DEFAULT NULL,
PRIMARY KEY (log_id),
KEY schedule_id (schedule_id),
KEY status (status),
KEY sent_date (sent_date),
KEY recipient_email (recipient_email)
) {$charset_collate};";
dbDelta( $sql );
}
/**
* Create event communication tracking table
*
* @param string $charset_collate Database charset and collation
*/
private static function create_tracking_table( $charset_collate ) {
global $wpdb;
$table_name = $wpdb->prefix . 'hvac_event_communication_tracking';
$sql = "CREATE TABLE {$table_name} (
tracking_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
event_id BIGINT(20) UNSIGNED NOT NULL,
attendee_id BIGINT(20) UNSIGNED NOT NULL,
schedule_id BIGINT(20) UNSIGNED NOT NULL,
email VARCHAR(255) NOT NULL,
sent_date DATETIME NOT NULL,
delivery_status VARCHAR(20) NOT NULL DEFAULT 'sent',
opened TINYINT(1) NOT NULL DEFAULT 0,
opened_date DATETIME DEFAULT NULL,
clicked TINYINT(1) NOT NULL DEFAULT 0,
clicked_date DATETIME DEFAULT NULL,
bounced TINYINT(1) NOT NULL DEFAULT 0,
bounce_reason TEXT DEFAULT NULL,
PRIMARY KEY (tracking_id),
UNIQUE KEY event_attendee_schedule (event_id, attendee_id, schedule_id),
KEY event_id (event_id),
KEY attendee_id (attendee_id),
KEY schedule_id (schedule_id),
KEY email (email),
KEY delivery_status (delivery_status),
KEY sent_date (sent_date)
) {$charset_collate};";
dbDelta( $sql );
}
/**
* Check if tables need to be updated
*
* @return bool True if update needed
*/
public static function needs_update() {
$installed_version = get_option( 'hvac_communication_db_version', '0' );
return version_compare( $installed_version, self::DB_VERSION, '<' );
}
/**
* Update database tables if needed
*/
public static function maybe_update() {
if ( self::needs_update() ) {
self::install();
}
}
/**
* Check if all required tables exist
*
* @return bool True if all tables exist
*/
public static function tables_exist() {
global $wpdb;
$required_tables = array(
$wpdb->prefix . 'hvac_communication_schedules',
$wpdb->prefix . 'hvac_communication_logs',
$wpdb->prefix . 'hvac_event_communication_tracking'
);
foreach ( $required_tables as $table ) {
if ( $wpdb->get_var( "SHOW TABLES LIKE '{$table}'" ) !== $table ) {
return false;
}
}
return true;
}
/**
* Drop all communication tables (for uninstall)
*/
public static function drop_tables() {
global $wpdb;
$tables = array(
$wpdb->prefix . 'hvac_communication_schedules',
$wpdb->prefix . 'hvac_communication_logs',
$wpdb->prefix . 'hvac_event_communication_tracking'
);
foreach ( $tables as $table ) {
$wpdb->query( "DROP TABLE IF EXISTS {$table}" );
}
delete_option( 'hvac_communication_db_version' );
if ( class_exists( 'HVAC_Logger' ) ) {
HVAC_Logger::info( 'Communication system database tables dropped', 'Communication Installer' );
}
}
/**
* Get table status information
*
* @return array Table status information
*/
public static function get_table_status() {
global $wpdb;
$tables = array(
'schedules' => $wpdb->prefix . 'hvac_communication_schedules',
'logs' => $wpdb->prefix . 'hvac_communication_logs',
'tracking' => $wpdb->prefix . 'hvac_event_communication_tracking'
);
$status = array();
foreach ( $tables as $key => $table_name ) {
$exists = $wpdb->get_var( "SHOW TABLES LIKE '{$table_name}'" ) === $table_name;
$count = 0;
if ( $exists ) {
$count = $wpdb->get_var( "SELECT COUNT(*) FROM {$table_name}" );
}
$status[$key] = array(
'table_name' => $table_name,
'exists' => $exists,
'record_count' => intval( $count )
);
}
$status['db_version'] = get_option( 'hvac_communication_db_version', '0' );
$status['current_version'] = self::DB_VERSION;
$status['needs_update'] = self::needs_update();
return $status;
}
/**
* Repair corrupted tables
*
* @return array Repair results
*/
public static function repair_tables() {
global $wpdb;
$tables = array(
$wpdb->prefix . 'hvac_communication_schedules',
$wpdb->prefix . 'hvac_communication_logs',
$wpdb->prefix . 'hvac_event_communication_tracking'
);
$results = array();
foreach ( $tables as $table ) {
$result = $wpdb->query( "REPAIR TABLE {$table}" );
$results[$table] = $result !== false;
}
return $results;
}
/**
* Optimize database tables
*
* @return array Optimization results
*/
public static function optimize_tables() {
global $wpdb;
$tables = array(
$wpdb->prefix . 'hvac_communication_schedules',
$wpdb->prefix . 'hvac_communication_logs',
$wpdb->prefix . 'hvac_event_communication_tracking'
);
$results = array();
foreach ( $tables as $table ) {
$result = $wpdb->query( "OPTIMIZE TABLE {$table}" );
$results[$table] = $result !== false;
}
return $results;
}
/**
* Get database size information
*
* @return array Database size information
*/
public static function get_database_size() {
global $wpdb;
$tables = array(
$wpdb->prefix . 'hvac_communication_schedules',
$wpdb->prefix . 'hvac_communication_logs',
$wpdb->prefix . 'hvac_event_communication_tracking'
);
$total_size = 0;
$table_sizes = array();
foreach ( $tables as $table ) {
$size_result = $wpdb->get_row(
$wpdb->prepare(
"SELECT
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'size_mb'
FROM information_schema.TABLES
WHERE table_schema = %s
AND table_name = %s",
DB_NAME,
$table
)
);
$size_mb = $size_result ? floatval( $size_result->size_mb ) : 0;
$table_sizes[$table] = $size_mb;
$total_size += $size_mb;
}
return array(
'total_size_mb' => round( $total_size, 2 ),
'table_sizes' => $table_sizes
);
}
/**
* Create default communication schedules
*/
public static function create_default_schedules() {
// This would create some default schedule templates
// For now, we'll just log that defaults would be created
if ( class_exists( 'HVAC_Logger' ) ) {
HVAC_Logger::info( 'Default communication schedules would be created here', 'Communication Installer' );
}
}
/**
* Migrate data from older versions
*
* @param string $from_version Version to migrate from
*/
public static function migrate_data( $from_version ) {
// Handle data migration between versions
// For now, this is a placeholder
if ( class_exists( 'HVAC_Logger' ) ) {
HVAC_Logger::info( "Data migration from version {$from_version} to " . self::DB_VERSION, 'Communication Installer' );
}
}
}

View file

@ -0,0 +1,467 @@
<?php
/**
* HVAC Community Events - Communication Logger
*
* Handles logging of communication schedule execution and delivery.
* Tracks sent emails, failures, and schedule performance.
*
* @package HVAC_Community_Events
* @subpackage Communication
* @version 1.0.0
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class HVAC_Communication_Logger
*
* Manages logging for communication schedules and delivery.
*/
class HVAC_Communication_Logger {
/**
* Database table names
*/
private $logs_table;
/**
* Constructor
*/
public function __construct() {
global $wpdb;
$this->logs_table = $wpdb->prefix . 'hvac_communication_logs';
}
/**
* Log a schedule execution
*
* @param int $schedule_id Schedule ID
* @param string $status Execution status ('sent', 'failed', 'skipped')
* @param array $details Additional execution details
* @return int|false Log ID on success, false on failure
*/
public function log_schedule_execution( $schedule_id, $status, $details = array() ) {
global $wpdb;
$log_data = array(
'schedule_id' => intval( $schedule_id ),
'status' => sanitize_text_field( $status ),
'sent_date' => current_time( 'mysql' ),
'recipient_count' => isset( $details['recipient_count'] ) ? intval( $details['recipient_count'] ) : 0,
'success_count' => isset( $details['success_count'] ) ? intval( $details['success_count'] ) : 0,
'error_count' => isset( $details['error_count'] ) ? intval( $details['error_count'] ) : 0,
'execution_time' => isset( $details['execution_time'] ) ? floatval( $details['execution_time'] ) : 0,
'details' => ! empty( $details ) ? wp_json_encode( $details ) : null
);
$formats = array( '%d', '%s', '%s', '%d', '%d', '%d', '%f', '%s' );
$result = $wpdb->insert( $this->logs_table, $log_data, $formats );
if ( $result === false ) {
if ( class_exists( 'HVAC_Logger' ) ) {
HVAC_Logger::error( 'Failed to log schedule execution: ' . $wpdb->last_error, 'Communication Logger' );
}
return false;
}
return $wpdb->insert_id;
}
/**
* Log individual email delivery
*
* @param int $schedule_id Schedule ID
* @param string $recipient_email Recipient email address
* @param string $status Delivery status ('sent', 'failed', 'bounced')
* @param array $details Additional delivery details
* @return int|false Log ID on success, false on failure
*/
public function log_email_delivery( $schedule_id, $recipient_email, $status, $details = array() ) {
global $wpdb;
$log_data = array(
'schedule_id' => intval( $schedule_id ),
'recipient_email' => sanitize_email( $recipient_email ),
'status' => sanitize_text_field( $status ),
'sent_date' => current_time( 'mysql' ),
'details' => ! empty( $details ) ? wp_json_encode( $details ) : null
);
$formats = array( '%d', '%s', '%s', '%s', '%s' );
$result = $wpdb->insert( $this->logs_table, $log_data, $formats );
return $result !== false ? $wpdb->insert_id : false;
}
/**
* Get execution logs for a schedule
*
* @param int $schedule_id Schedule ID
* @param array $args Query arguments
* @return array Array of log entries
*/
public function get_schedule_logs( $schedule_id, $args = array() ) {
global $wpdb;
$defaults = array(
'limit' => 50,
'offset' => 0,
'status' => null,
'date_from' => null,
'date_to' => null
);
$args = wp_parse_args( $args, $defaults );
$where_clauses = array( 'schedule_id = %d' );
$where_values = array( intval( $schedule_id ) );
// Status filter
if ( ! empty( $args['status'] ) ) {
$where_clauses[] = 'status = %s';
$where_values[] = $args['status'];
}
// Date range filters
if ( ! empty( $args['date_from'] ) ) {
$where_clauses[] = 'sent_date >= %s';
$where_values[] = $args['date_from'];
}
if ( ! empty( $args['date_to'] ) ) {
$where_clauses[] = 'sent_date <= %s';
$where_values[] = $args['date_to'];
}
$where_sql = implode( ' AND ', $where_clauses );
$sql = "SELECT * FROM {$this->logs_table}
WHERE {$where_sql}
ORDER BY sent_date DESC
LIMIT %d OFFSET %d";
$where_values[] = intval( $args['limit'] );
$where_values[] = intval( $args['offset'] );
$logs = $wpdb->get_results( $wpdb->prepare( $sql, $where_values ), ARRAY_A );
// Decode JSON details
foreach ( $logs as &$log ) {
if ( ! empty( $log['details'] ) ) {
$log['details'] = json_decode( $log['details'], true );
}
}
return $logs;
}
/**
* Get logs for all schedules with filtering
*
* @param array $args Query arguments
* @return array Array of log entries with schedule info
*/
public function get_all_logs( $args = array() ) {
global $wpdb;
$defaults = array(
'limit' => 50,
'offset' => 0,
'trainer_id' => null,
'status' => null,
'date_from' => null,
'date_to' => null
);
$args = wp_parse_args( $args, $defaults );
$schedules_table = $wpdb->prefix . 'hvac_communication_schedules';
$where_clauses = array();
$where_values = array();
// Trainer filter
if ( ! empty( $args['trainer_id'] ) ) {
$where_clauses[] = 's.trainer_id = %d';
$where_values[] = intval( $args['trainer_id'] );
}
// Status filter
if ( ! empty( $args['status'] ) ) {
$where_clauses[] = 'l.status = %s';
$where_values[] = $args['status'];
}
// Date range filters
if ( ! empty( $args['date_from'] ) ) {
$where_clauses[] = 'l.sent_date >= %s';
$where_values[] = $args['date_from'];
}
if ( ! empty( $args['date_to'] ) ) {
$where_clauses[] = 'l.sent_date <= %s';
$where_values[] = $args['date_to'];
}
$where_sql = ! empty( $where_clauses ) ? 'WHERE ' . implode( ' AND ', $where_clauses ) : '';
$sql = "SELECT l.*,
s.trainer_id,
s.event_id,
s.template_id,
s.trigger_type,
e.post_title as event_name,
t.post_title as template_name
FROM {$this->logs_table} l
LEFT JOIN {$schedules_table} s ON l.schedule_id = s.schedule_id
LEFT JOIN {$wpdb->posts} e ON s.event_id = e.ID
LEFT JOIN {$wpdb->posts} t ON s.template_id = t.ID
{$where_sql}
ORDER BY l.sent_date DESC
LIMIT %d OFFSET %d";
$where_values[] = intval( $args['limit'] );
$where_values[] = intval( $args['offset'] );
$logs = $wpdb->get_results( $wpdb->prepare( $sql, $where_values ), ARRAY_A );
// Decode JSON details
foreach ( $logs as &$log ) {
if ( ! empty( $log['details'] ) ) {
$log['details'] = json_decode( $log['details'], true );
}
}
return $logs;
}
/**
* Get summary statistics for communication logs
*
* @param int|null $trainer_id Optional trainer ID filter
* @param string|null $date_from Optional start date filter
* @param string|null $date_to Optional end date filter
* @return array Statistics array
*/
public function get_statistics( $trainer_id = null, $date_from = null, $date_to = null ) {
global $wpdb;
$schedules_table = $wpdb->prefix . 'hvac_communication_schedules';
$where_clauses = array();
$where_values = array();
if ( ! empty( $trainer_id ) ) {
$where_clauses[] = 's.trainer_id = %d';
$where_values[] = intval( $trainer_id );
}
if ( ! empty( $date_from ) ) {
$where_clauses[] = 'l.sent_date >= %s';
$where_values[] = $date_from;
}
if ( ! empty( $date_to ) ) {
$where_clauses[] = 'l.sent_date <= %s';
$where_values[] = $date_to;
}
$where_sql = ! empty( $where_clauses ) ? 'WHERE ' . implode( ' AND ', $where_clauses ) : '';
$sql = "SELECT
COUNT(*) as total_executions,
COUNT(CASE WHEN l.status = 'sent' THEN 1 END) as successful_executions,
COUNT(CASE WHEN l.status = 'failed' THEN 1 END) as failed_executions,
COUNT(CASE WHEN l.status = 'skipped' THEN 1 END) as skipped_executions,
SUM(l.recipient_count) as total_recipients,
SUM(l.success_count) as total_emails_sent,
SUM(l.error_count) as total_email_errors,
AVG(l.execution_time) as avg_execution_time
FROM {$this->logs_table} l
LEFT JOIN {$schedules_table} s ON l.schedule_id = s.schedule_id
{$where_sql}";
$stats = $wpdb->get_row(
empty( $where_values ) ? $sql : $wpdb->prepare( $sql, $where_values ),
ARRAY_A
);
// Ensure numeric values
foreach ( $stats as $key => $value ) {
if ( in_array( $key, array( 'avg_execution_time' ) ) ) {
$stats[$key] = floatval( $value );
} else {
$stats[$key] = intval( $value );
}
}
return $stats;
}
/**
* Get recent execution activity
*
* @param int $limit Number of recent activities to retrieve
* @param int|null $trainer_id Optional trainer ID filter
* @return array Array of recent activities
*/
public function get_recent_activity( $limit = 10, $trainer_id = null ) {
global $wpdb;
$schedules_table = $wpdb->prefix . 'hvac_communication_schedules';
$where_clause = '';
$where_values = array();
if ( ! empty( $trainer_id ) ) {
$where_clause = 'WHERE s.trainer_id = %d';
$where_values[] = intval( $trainer_id );
}
$sql = "SELECT l.*,
s.trainer_id,
e.post_title as event_name,
t.post_title as template_name
FROM {$this->logs_table} l
LEFT JOIN {$schedules_table} s ON l.schedule_id = s.schedule_id
LEFT JOIN {$wpdb->posts} e ON s.event_id = e.ID
LEFT JOIN {$wpdb->posts} t ON s.template_id = t.ID
{$where_clause}
ORDER BY l.sent_date DESC
LIMIT %d";
$where_values[] = intval( $limit );
$activities = $wpdb->get_results( $wpdb->prepare( $sql, $where_values ), ARRAY_A );
// Decode JSON details and format for display
foreach ( $activities as &$activity ) {
if ( ! empty( $activity['details'] ) ) {
$activity['details'] = json_decode( $activity['details'], true );
}
// Add human-readable time
$activity['time_ago'] = human_time_diff( strtotime( $activity['sent_date'] ), current_time( 'timestamp' ) ) . ' ago';
}
return $activities;
}
/**
* Clean up old log entries
*
* @param int $days_to_keep Number of days to keep logs (default 90)
* @return int Number of entries deleted
*/
public function cleanup_old_logs( $days_to_keep = 90 ) {
global $wpdb;
$cutoff_date = date( 'Y-m-d H:i:s', strtotime( "-{$days_to_keep} days" ) );
$deleted = $wpdb->query( $wpdb->prepare(
"DELETE FROM {$this->logs_table} WHERE sent_date < %s",
$cutoff_date
) );
if ( $deleted !== false && class_exists( 'HVAC_Logger' ) ) {
HVAC_Logger::info( "Cleaned up {$deleted} old communication log entries", 'Communication Logger' );
}
return intval( $deleted );
}
/**
* Export logs to CSV format
*
* @param array $args Export arguments
* @return string CSV content
*/
public function export_logs_csv( $args = array() ) {
$logs = $this->get_all_logs( $args );
$csv_header = array(
'Date',
'Schedule ID',
'Event',
'Template',
'Status',
'Recipients',
'Successful',
'Errors',
'Execution Time (s)'
);
$csv_data = array();
$csv_data[] = $csv_header;
foreach ( $logs as $log ) {
$csv_data[] = array(
$log['sent_date'],
$log['schedule_id'],
$log['event_name'] ?: 'N/A',
$log['template_name'] ?: 'N/A',
$log['status'],
$log['recipient_count'],
$log['success_count'],
$log['error_count'],
$log['execution_time']
);
}
// Convert to CSV string
$csv_content = '';
foreach ( $csv_data as $row ) {
$csv_content .= '"' . implode( '","', $row ) . '"' . "\n";
}
return $csv_content;
}
/**
* Get performance metrics for schedules
*
* @param int|null $trainer_id Optional trainer ID filter
* @return array Performance metrics
*/
public function get_performance_metrics( $trainer_id = null ) {
global $wpdb;
$schedules_table = $wpdb->prefix . 'hvac_communication_schedules';
$where_clause = '';
$where_values = array();
if ( ! empty( $trainer_id ) ) {
$where_clause = 'WHERE s.trainer_id = %d';
$where_values[] = intval( $trainer_id );
}
// Get delivery success rate
$sql = "SELECT
COUNT(*) as total_schedules,
AVG(CASE WHEN l.status = 'sent' THEN 100.0 ELSE 0.0 END) as success_rate,
AVG(l.execution_time) as avg_execution_time,
SUM(l.recipient_count) / COUNT(*) as avg_recipients_per_execution
FROM {$this->logs_table} l
LEFT JOIN {$schedules_table} s ON l.schedule_id = s.schedule_id
{$where_clause}";
$metrics = $wpdb->get_row(
empty( $where_values ) ? $sql : $wpdb->prepare( $sql, $where_values ),
ARRAY_A
);
// Format metrics
$metrics['success_rate'] = round( floatval( $metrics['success_rate'] ), 2 );
$metrics['avg_execution_time'] = round( floatval( $metrics['avg_execution_time'] ), 3 );
$metrics['avg_recipients_per_execution'] = round( floatval( $metrics['avg_recipients_per_execution'] ), 1 );
return $metrics;
}
}

View file

@ -0,0 +1,603 @@
<?php
/**
* HVAC Community Events - Communication Schedule Manager
*
* Handles CRUD operations for communication schedules.
* Manages database interactions and schedule validation.
*
* @package HVAC_Community_Events
* @subpackage Communication
* @version 1.0.0
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class HVAC_Communication_Schedule_Manager
*
* Manages communication schedule database operations.
*/
class HVAC_Communication_Schedule_Manager {
/**
* Database table names
*/
private $schedules_table;
private $logs_table;
private $tracking_table;
/**
* Constructor
*/
public function __construct() {
global $wpdb;
$this->schedules_table = $wpdb->prefix . 'hvac_communication_schedules';
$this->logs_table = $wpdb->prefix . 'hvac_communication_logs';
$this->tracking_table = $wpdb->prefix . 'hvac_event_communication_tracking';
}
/**
* Save a communication schedule
*
* @param array $schedule_data Schedule configuration
* @return int|false Schedule ID on success, false on failure
*/
public function save_schedule( $schedule_data ) {
global $wpdb;
$data = array(
'trainer_id' => intval( $schedule_data['trainer_id'] ),
'event_id' => ! empty( $schedule_data['event_id'] ) ? intval( $schedule_data['event_id'] ) : null,
'template_id' => intval( $schedule_data['template_id'] ),
'schedule_type' => isset( $schedule_data['schedule_type'] ) ? $schedule_data['schedule_type'] : 'time_based',
'trigger_type' => sanitize_text_field( $schedule_data['trigger_type'] ),
'trigger_value' => intval( $schedule_data['trigger_value'] ),
'trigger_unit' => sanitize_text_field( $schedule_data['trigger_unit'] ),
'status' => isset( $schedule_data['status'] ) ? sanitize_text_field( $schedule_data['status'] ) : 'active',
'target_audience' => sanitize_text_field( $schedule_data['target_audience'] ),
'custom_recipient_list' => ! empty( $schedule_data['custom_recipient_list'] ) ?
sanitize_textarea_field( $schedule_data['custom_recipient_list'] ) : null,
'conditions' => ! empty( $schedule_data['conditions'] ) ?
wp_json_encode( $schedule_data['conditions'] ) : null,
'next_run' => ! empty( $schedule_data['next_run'] ) ?
sanitize_text_field( $schedule_data['next_run'] ) : null,
'is_recurring' => isset( $schedule_data['is_recurring'] ) ?
(int) $schedule_data['is_recurring'] : 0,
'recurring_interval' => ! empty( $schedule_data['recurring_interval'] ) ?
intval( $schedule_data['recurring_interval'] ) : null,
'recurring_unit' => ! empty( $schedule_data['recurring_unit'] ) ?
sanitize_text_field( $schedule_data['recurring_unit'] ) : null,
'max_runs' => ! empty( $schedule_data['max_runs'] ) ?
intval( $schedule_data['max_runs'] ) : null
);
$formats = array(
'%d', // trainer_id
'%d', // event_id
'%d', // template_id
'%s', // schedule_type
'%s', // trigger_type
'%d', // trigger_value
'%s', // trigger_unit
'%s', // status
'%s', // target_audience
'%s', // custom_recipient_list
'%s', // conditions
'%s', // next_run
'%d', // is_recurring
'%d', // recurring_interval
'%s', // recurring_unit
'%d' // max_runs
);
$result = $wpdb->insert( $this->schedules_table, $data, $formats );
if ( $result === false ) {
if ( class_exists( 'HVAC_Logger' ) ) {
HVAC_Logger::error( 'Failed to save communication schedule: ' . $wpdb->last_error, 'Schedule Manager' );
}
return false;
}
return $wpdb->insert_id;
}
/**
* Update a communication schedule
*
* @param int $schedule_id Schedule ID
* @param array $schedule_data Updated schedule data
* @return bool Success status
*/
public function update_schedule( $schedule_id, $schedule_data ) {
global $wpdb;
$data = array();
$formats = array();
// Only update provided fields
$allowed_fields = array(
'event_id' => '%d',
'template_id' => '%d',
'schedule_type' => '%s',
'trigger_type' => '%s',
'trigger_value' => '%d',
'trigger_unit' => '%s',
'status' => '%s',
'target_audience' => '%s',
'custom_recipient_list' => '%s',
'conditions' => '%s',
'next_run' => '%s',
'is_recurring' => '%d',
'recurring_interval' => '%d',
'recurring_unit' => '%s',
'max_runs' => '%d'
);
foreach ( $allowed_fields as $field => $format ) {
if ( array_key_exists( $field, $schedule_data ) ) {
if ( $field === 'conditions' && ! empty( $schedule_data[$field] ) ) {
$data[$field] = wp_json_encode( $schedule_data[$field] );
} elseif ( in_array( $format, array( '%d' ) ) ) {
$data[$field] = intval( $schedule_data[$field] );
} else {
$data[$field] = sanitize_text_field( $schedule_data[$field] );
}
$formats[] = $format;
}
}
// Add modified timestamp
$data['modified_date'] = current_time( 'mysql' );
$formats[] = '%s';
$result = $wpdb->update(
$this->schedules_table,
$data,
array( 'schedule_id' => intval( $schedule_id ) ),
$formats,
array( '%d' )
);
return $result !== false;
}
/**
* Get a communication schedule by ID
*
* @param int $schedule_id Schedule ID
* @return array|null Schedule data or null if not found
*/
public function get_schedule( $schedule_id ) {
global $wpdb;
$schedule = $wpdb->get_row( $wpdb->prepare(
"SELECT * FROM {$this->schedules_table} WHERE schedule_id = %d",
intval( $schedule_id )
), ARRAY_A );
if ( $schedule && ! empty( $schedule['conditions'] ) ) {
$schedule['conditions'] = json_decode( $schedule['conditions'], true );
}
return $schedule;
}
/**
* Get schedules by trainer
*
* @param int $trainer_id Trainer user ID
* @param int $event_id Optional specific event ID
* @return array Array of schedules
*/
public function get_schedules_by_trainer( $trainer_id, $event_id = null ) {
global $wpdb;
$where_clause = "WHERE trainer_id = %d";
$params = array( intval( $trainer_id ) );
if ( $event_id ) {
$where_clause .= " AND event_id = %d";
$params[] = intval( $event_id );
}
$schedules = $wpdb->get_results( $wpdb->prepare(
"SELECT s.*,
t.post_title as template_name,
e.post_title as event_name,
e.post_status as event_status
FROM {$this->schedules_table} s
LEFT JOIN {$wpdb->posts} t ON s.template_id = t.ID
LEFT JOIN {$wpdb->posts} e ON s.event_id = e.ID
{$where_clause}
ORDER BY s.created_date DESC",
$params
), ARRAY_A );
// Process conditions field
foreach ( $schedules as &$schedule ) {
if ( ! empty( $schedule['conditions'] ) ) {
$schedule['conditions'] = json_decode( $schedule['conditions'], true );
}
}
return $schedules;
}
/**
* Get schedules by event
*
* @param int $event_id Event ID
* @return array Array of schedules
*/
public function get_schedules_by_event( $event_id ) {
global $wpdb;
$schedules = $wpdb->get_results( $wpdb->prepare(
"SELECT s.*,
t.post_title as template_name
FROM {$this->schedules_table} s
LEFT JOIN {$wpdb->posts} t ON s.template_id = t.ID
WHERE s.event_id = %d
ORDER BY s.trigger_type, s.trigger_value",
intval( $event_id )
), ARRAY_A );
// Process conditions field
foreach ( $schedules as &$schedule ) {
if ( ! empty( $schedule['conditions'] ) ) {
$schedule['conditions'] = json_decode( $schedule['conditions'], true );
}
}
return $schedules;
}
/**
* Get active schedules
*
* @return array Array of active schedules
*/
public function get_active_schedules() {
global $wpdb;
return $wpdb->get_results(
"SELECT * FROM {$this->schedules_table}
WHERE status = 'active'
ORDER BY next_run ASC",
ARRAY_A
);
}
/**
* Get due schedules
*
* @return array Array of schedules that are due for execution
*/
public function get_due_schedules() {
global $wpdb;
$current_time = current_time( 'mysql' );
$schedules = $wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$this->schedules_table}
WHERE status = 'active'
AND next_run IS NOT NULL
AND next_run <= %s
AND (max_runs IS NULL OR run_count < max_runs)
ORDER BY next_run ASC",
$current_time
), ARRAY_A );
return $schedules;
}
/**
* Delete a communication schedule
*
* @param int $schedule_id Schedule ID
* @return bool Success status
*/
public function delete_schedule( $schedule_id ) {
global $wpdb;
// Delete associated logs first (foreign key constraint)
$wpdb->delete(
$this->logs_table,
array( 'schedule_id' => intval( $schedule_id ) ),
array( '%d' )
);
// Delete the schedule
$result = $wpdb->delete(
$this->schedules_table,
array( 'schedule_id' => intval( $schedule_id ) ),
array( '%d' )
);
return $result !== false;
}
/**
* Update schedule run tracking
*
* @param int $schedule_id Schedule ID
* @return bool Success status
*/
public function update_schedule_run_tracking( $schedule_id ) {
global $wpdb;
$schedule = $this->get_schedule( $schedule_id );
if ( ! $schedule ) {
return false;
}
$data = array(
'last_run' => current_time( 'mysql' ),
'run_count' => intval( $schedule['run_count'] ) + 1
);
// Calculate next run if recurring
if ( $schedule['is_recurring'] ) {
$next_run = $this->calculate_next_recurring_run( $schedule );
if ( $next_run ) {
$data['next_run'] = $next_run;
}
} else {
// Mark as completed if not recurring
$data['status'] = 'completed';
$data['next_run'] = null;
}
return $this->update_schedule( $schedule_id, $data );
}
/**
* Calculate next recurring run time
*
* @param array $schedule Schedule data
* @return string|null Next run time or null
*/
private function calculate_next_recurring_run( $schedule ) {
if ( ! $schedule['is_recurring'] || ! $schedule['recurring_interval'] ) {
return null;
}
$interval = $schedule['recurring_interval'];
$unit = $schedule['recurring_unit'];
$current_time = current_time( 'timestamp' );
switch ( $unit ) {
case 'days':
$next_time = $current_time + ( $interval * DAY_IN_SECONDS );
break;
case 'weeks':
$next_time = $current_time + ( $interval * WEEK_IN_SECONDS );
break;
case 'months':
$next_time = strtotime( "+{$interval} months", $current_time );
break;
default:
return null;
}
return date( 'Y-m-d H:i:s', $next_time );
}
/**
* Validate schedule data
*
* @param array $schedule_data Schedule data to validate
* @return bool|WP_Error True if valid, WP_Error if invalid
*/
public function validate_schedule_data( $schedule_data ) {
// Required fields
$required_fields = array( 'trainer_id', 'template_id', 'trigger_type', 'target_audience' );
foreach ( $required_fields as $field ) {
if ( empty( $schedule_data[$field] ) ) {
return new WP_Error( 'missing_field', sprintf( __( 'Required field missing: %s', 'hvac-community-events' ), $field ) );
}
}
// Validate trainer exists and has permission
$trainer = get_user_by( 'id', $schedule_data['trainer_id'] );
if ( ! $trainer || ! in_array( 'hvac_trainer', $trainer->roles ) ) {
return new WP_Error( 'invalid_trainer', __( 'Invalid trainer specified.', 'hvac-community-events' ) );
}
// Validate template exists and belongs to trainer
$template = get_post( $schedule_data['template_id'] );
if ( ! $template || $template->post_type !== 'hvac_email_template' ) {
return new WP_Error( 'invalid_template', __( 'Invalid template specified.', 'hvac-community-events' ) );
}
if ( $template->post_author != $schedule_data['trainer_id'] && ! current_user_can( 'edit_others_posts' ) ) {
return new WP_Error( 'template_permission', __( 'You do not have permission to use this template.', 'hvac-community-events' ) );
}
// Validate event if specified
if ( ! empty( $schedule_data['event_id'] ) ) {
$event = get_post( $schedule_data['event_id'] );
if ( ! $event || $event->post_type !== 'tribe_events' ) {
return new WP_Error( 'invalid_event', __( 'Invalid event specified.', 'hvac-community-events' ) );
}
// Check if trainer owns the event
if ( $event->post_author != $schedule_data['trainer_id'] && ! current_user_can( 'edit_others_posts' ) ) {
return new WP_Error( 'event_permission', __( 'You do not have permission to schedule communications for this event.', 'hvac-community-events' ) );
}
}
// Validate trigger settings
$valid_trigger_types = array( 'before_event', 'after_event', 'on_registration', 'custom_date' );
if ( ! in_array( $schedule_data['trigger_type'], $valid_trigger_types ) ) {
return new WP_Error( 'invalid_trigger_type', __( 'Invalid trigger type specified.', 'hvac-community-events' ) );
}
$valid_trigger_units = array( 'minutes', 'hours', 'days', 'weeks' );
if ( ! in_array( $schedule_data['trigger_unit'], $valid_trigger_units ) ) {
return new WP_Error( 'invalid_trigger_unit', __( 'Invalid trigger unit specified.', 'hvac-community-events' ) );
}
// Validate audience settings
$valid_audiences = array( 'all_attendees', 'confirmed_attendees', 'pending_attendees', 'custom_list' );
if ( ! in_array( $schedule_data['target_audience'], $valid_audiences ) ) {
return new WP_Error( 'invalid_audience', __( 'Invalid target audience specified.', 'hvac-community-events' ) );
}
// Validate custom recipient list if specified
if ( $schedule_data['target_audience'] === 'custom_list' && empty( $schedule_data['custom_recipient_list'] ) ) {
return new WP_Error( 'missing_recipients', __( 'Custom recipient list is required when target audience is set to custom list.', 'hvac-community-events' ) );
}
// Validate recurring settings
if ( ! empty( $schedule_data['is_recurring'] ) ) {
if ( empty( $schedule_data['recurring_interval'] ) || empty( $schedule_data['recurring_unit'] ) ) {
return new WP_Error( 'invalid_recurring', __( 'Recurring interval and unit are required for recurring schedules.', 'hvac-community-events' ) );
}
$valid_recurring_units = array( 'days', 'weeks', 'months' );
if ( ! in_array( $schedule_data['recurring_unit'], $valid_recurring_units ) ) {
return new WP_Error( 'invalid_recurring_unit', __( 'Invalid recurring unit specified.', 'hvac-community-events' ) );
}
}
return true;
}
/**
* Check for schedule conflicts
*
* @param array $schedule_data Schedule data to check
* @return bool|WP_Error True if no conflicts, WP_Error if conflicts found
*/
public function check_schedule_conflicts( $schedule_data ) {
global $wpdb;
// Check for duplicate schedules with same trigger settings
$existing = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$this->schedules_table}
WHERE trainer_id = %d
AND event_id = %d
AND template_id = %d
AND trigger_type = %s
AND trigger_value = %d
AND trigger_unit = %s
AND status = 'active'",
$schedule_data['trainer_id'],
$schedule_data['event_id'] ?? 0,
$schedule_data['template_id'],
$schedule_data['trigger_type'],
$schedule_data['trigger_value'],
$schedule_data['trigger_unit']
) );
if ( $existing > 0 ) {
return new WP_Error( 'duplicate_schedule', __( 'A schedule with identical settings already exists.', 'hvac-community-events' ) );
}
return true;
}
/**
* Check if user can edit schedule
*
* @param int $schedule_id Schedule ID
* @return bool Whether user can edit the schedule
*/
public function user_can_edit_schedule( $schedule_id ) {
$schedule = $this->get_schedule( $schedule_id );
if ( ! $schedule ) {
return false;
}
$current_user_id = get_current_user_id();
// Owner can edit
if ( $schedule['trainer_id'] == $current_user_id ) {
return true;
}
// Admins can edit others' schedules
if ( current_user_can( 'edit_others_posts' ) ) {
return true;
}
return false;
}
/**
* Get available templates for scheduling
*
* @param int $trainer_id Trainer user ID
* @return array Array of available templates
*/
public function get_available_templates( $trainer_id ) {
$templates_manager = new HVAC_Communication_Templates();
return $templates_manager->get_user_templates( $trainer_id );
}
/**
* Validate template compatibility with schedule type
*
* @param int $template_id Template ID
* @param string $schedule_type Schedule type
* @return bool|WP_Error True if compatible, WP_Error if not
*/
public function validate_template_compatibility( $template_id, $schedule_type ) {
$template = get_post( $template_id );
if ( ! $template || $template->post_type !== 'hvac_email_template' ) {
return new WP_Error( 'invalid_template', __( 'Invalid template specified.', 'hvac-community-events' ) );
}
// Check for required placeholders based on schedule type
$required_placeholders = array( '{attendee_name}', '{event_title}' );
if ( $schedule_type === 'event_based' ) {
$required_placeholders[] = '{event_date}';
$required_placeholders[] = '{event_time}';
}
foreach ( $required_placeholders as $placeholder ) {
if ( strpos( $template->post_content, $placeholder ) === false ) {
return new WP_Error( 'missing_placeholder',
sprintf( __( 'Template missing required placeholder: %s', 'hvac-community-events' ), $placeholder )
);
}
}
return true;
}
/**
* Get schedule statistics for a trainer
*
* @param int $trainer_id Trainer user ID
* @return array Statistics array
*/
public function get_trainer_schedule_stats( $trainer_id ) {
global $wpdb;
$stats = $wpdb->get_row( $wpdb->prepare(
"SELECT
COUNT(*) as total_schedules,
COUNT(CASE WHEN status = 'active' THEN 1 END) as active_schedules,
COUNT(CASE WHEN status = 'paused' THEN 1 END) as paused_schedules,
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_schedules,
SUM(run_count) as total_executions
FROM {$this->schedules_table}
WHERE trainer_id = %d",
$trainer_id
), ARRAY_A );
return $stats;
}
}

View file

@ -0,0 +1,596 @@
<?php
/**
* HVAC Community Events - Communication Scheduler
*
* Main controller for automated communication scheduling system.
* Handles creation, management, and execution of scheduled communications.
*
* @package HVAC_Community_Events
* @subpackage Communication
* @version 1.0.0
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class HVAC_Communication_Scheduler
*
* Core scheduling system for automated email communications.
*/
class HVAC_Communication_Scheduler {
/**
* Singleton instance
*/
private static $instance = null;
/**
* Schedule manager instance
*/
private $schedule_manager;
/**
* Trigger engine instance
*/
private $trigger_engine;
/**
* Communication logger instance
*/
private $logger;
/**
* Get singleton instance
*/
public static function instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->init_dependencies();
$this->register_hooks();
// Debug logging
if ( class_exists( 'HVAC_Logger' ) ) {
HVAC_Logger::info( 'HVAC_Communication_Scheduler initialized', 'Scheduler' );
}
}
/**
* Initialize dependencies
*/
private function init_dependencies() {
require_once HVAC_CE_PLUGIN_DIR . 'includes/communication/class-communication-schedule-manager.php';
require_once HVAC_CE_PLUGIN_DIR . 'includes/communication/class-communication-trigger-engine.php';
require_once HVAC_CE_PLUGIN_DIR . 'includes/communication/class-communication-logger.php';
$this->schedule_manager = new HVAC_Communication_Schedule_Manager();
$this->trigger_engine = new HVAC_Communication_Trigger_Engine();
$this->logger = new HVAC_Communication_Logger();
}
/**
* Register WordPress hooks
*/
private function register_hooks() {
// Cron hooks
add_action( 'hvac_process_communication_schedules', array( $this, 'process_scheduled_communications' ) );
// Event-based triggers
add_action( 'tribe_events_event_save_after', array( $this, 'on_event_saved' ) );
add_action( 'event_tickets_after_add_attendee', array( $this, 'on_attendee_registered' ) );
add_action( 'wp', array( $this, 'check_event_date_changes' ) );
// AJAX handlers
add_action( 'wp_ajax_hvac_create_schedule', array( $this, 'ajax_create_schedule' ) );
add_action( 'wp_ajax_hvac_update_schedule', array( $this, 'ajax_update_schedule' ) );
add_action( 'wp_ajax_hvac_delete_schedule', array( $this, 'ajax_delete_schedule' ) );
add_action( 'wp_ajax_hvac_get_schedules', array( $this, 'ajax_get_schedules' ) );
add_action( 'wp_ajax_hvac_toggle_schedule', array( $this, 'ajax_toggle_schedule' ) );
add_action( 'wp_ajax_hvac_preview_recipients', array( $this, 'ajax_preview_recipients' ) );
// Custom cron schedules
add_filter( 'cron_schedules', array( $this, 'add_custom_cron_schedules' ) );
// Initialize cron if not scheduled
add_action( 'wp_loaded', array( $this, 'setup_cron_schedules' ) );
}
/**
* Add custom cron schedules
*/
public function add_custom_cron_schedules( $schedules ) {
$schedules['hvac_every_5_minutes'] = array(
'interval' => 300,
'display' => __( 'Every 5 minutes', 'hvac-community-events' )
);
$schedules['hvac_every_15_minutes'] = array(
'interval' => 900,
'display' => __( 'Every 15 minutes', 'hvac-community-events' )
);
return $schedules;
}
/**
* Setup cron schedules
*/
public function setup_cron_schedules() {
if ( ! wp_next_scheduled( 'hvac_process_communication_schedules' ) ) {
wp_schedule_event( time(), 'hvac_every_15_minutes', 'hvac_process_communication_schedules' );
if ( class_exists( 'HVAC_Logger' ) ) {
HVAC_Logger::info( 'Communication scheduler cron job set up', 'Scheduler' );
}
}
}
/**
* Create a new communication schedule
*
* @param array $schedule_data Schedule configuration
* @return int|WP_Error Schedule ID on success, WP_Error on failure
*/
public function create_schedule( $schedule_data ) {
// Validate schedule data
$validation_result = $this->schedule_manager->validate_schedule_data( $schedule_data );
if ( is_wp_error( $validation_result ) ) {
return $validation_result;
}
// Check for conflicts
$conflict_check = $this->schedule_manager->check_schedule_conflicts( $schedule_data );
if ( is_wp_error( $conflict_check ) ) {
return $conflict_check;
}
// Calculate next run time
$next_run = $this->calculate_next_run_time( $schedule_data );
$schedule_data['next_run'] = $next_run;
// Save schedule
$schedule_id = $this->schedule_manager->save_schedule( $schedule_data );
if ( $schedule_id && class_exists( 'HVAC_Logger' ) ) {
HVAC_Logger::info( "Communication schedule created: ID $schedule_id", 'Scheduler' );
}
return $schedule_id;
}
/**
* Update an existing communication schedule
*
* @param int $schedule_id Schedule ID
* @param array $schedule_data Updated schedule configuration
* @return bool|WP_Error Success status
*/
public function update_schedule( $schedule_id, $schedule_data ) {
// Verify ownership
if ( ! $this->schedule_manager->user_can_edit_schedule( $schedule_id ) ) {
return new WP_Error( 'permission_denied', __( 'You do not have permission to edit this schedule.', 'hvac-community-events' ) );
}
// Validate data
$validation_result = $this->schedule_manager->validate_schedule_data( $schedule_data );
if ( is_wp_error( $validation_result ) ) {
return $validation_result;
}
// Recalculate next run time if trigger settings changed
$existing_schedule = $this->schedule_manager->get_schedule( $schedule_id );
$trigger_changed = (
$existing_schedule['trigger_type'] !== $schedule_data['trigger_type'] ||
$existing_schedule['trigger_value'] !== $schedule_data['trigger_value'] ||
$existing_schedule['trigger_unit'] !== $schedule_data['trigger_unit']
);
if ( $trigger_changed ) {
$schedule_data['next_run'] = $this->calculate_next_run_time( $schedule_data );
}
return $this->schedule_manager->update_schedule( $schedule_id, $schedule_data );
}
/**
* Delete a communication schedule
*
* @param int $schedule_id Schedule ID
* @return bool|WP_Error Success status
*/
public function delete_schedule( $schedule_id ) {
// Verify ownership
if ( ! $this->schedule_manager->user_can_edit_schedule( $schedule_id ) ) {
return new WP_Error( 'permission_denied', __( 'You do not have permission to delete this schedule.', 'hvac-community-events' ) );
}
$result = $this->schedule_manager->delete_schedule( $schedule_id );
if ( $result && class_exists( 'HVAC_Logger' ) ) {
HVAC_Logger::info( "Communication schedule deleted: ID $schedule_id", 'Scheduler' );
}
return $result;
}
/**
* Get schedules for a trainer
*
* @param int $trainer_id Trainer user ID
* @param int $event_id Optional specific event ID
* @return array Array of schedules
*/
public function get_trainer_schedules( $trainer_id, $event_id = null ) {
return $this->schedule_manager->get_schedules_by_trainer( $trainer_id, $event_id );
}
/**
* Process all due scheduled communications
*/
public function process_scheduled_communications() {
$due_schedules = $this->schedule_manager->get_due_schedules();
if ( class_exists( 'HVAC_Logger' ) ) {
HVAC_Logger::info( 'Processing ' . count( $due_schedules ) . ' due communication schedules', 'Scheduler' );
}
foreach ( $due_schedules as $schedule ) {
$this->execute_schedule( $schedule['schedule_id'] );
}
}
/**
* Calculate next run time for a schedule
*
* @param array $schedule Schedule configuration
* @return string MySQL datetime string
*/
public function calculate_next_run_time( $schedule ) {
if ( ! empty( $schedule['event_id'] ) ) {
// Event-based scheduling
$event_date = get_post_meta( $schedule['event_id'], '_EventStartDate', true );
if ( ! $event_date ) {
return null;
}
return $this->trigger_engine->calculate_trigger_time( $event_date, $schedule );
} else {
// Immediate or custom date scheduling
if ( $schedule['trigger_type'] === 'custom_date' && ! empty( $schedule['custom_date'] ) ) {
return $schedule['custom_date'];
} elseif ( $schedule['trigger_type'] === 'on_registration' ) {
// This will be triggered immediately on registration
return null;
}
}
return null;
}
/**
* Execute a specific schedule
*
* @param int $schedule_id Schedule ID
* @return bool Success status
*/
public function execute_schedule( $schedule_id ) {
$schedule = $this->schedule_manager->get_schedule( $schedule_id );
if ( ! $schedule ) {
return false;
}
try {
// Get recipients
$recipients = $this->trigger_engine->get_schedule_recipients( $schedule );
if ( empty( $recipients ) ) {
$this->logger->log_schedule_execution( $schedule_id, 'skipped', array(
'reason' => 'No recipients found'
) );
return true;
}
// Execute communication
$result = $this->trigger_engine->execute_communication( $schedule, $recipients );
// Update schedule run tracking
$this->schedule_manager->update_schedule_run_tracking( $schedule_id );
// Log execution
$this->logger->log_schedule_execution( $schedule_id, 'sent', array(
'recipient_count' => count( $recipients ),
'success' => $result
) );
return $result;
} catch ( Exception $e ) {
$this->logger->log_schedule_execution( $schedule_id, 'failed', array(
'error' => $e->getMessage()
) );
if ( class_exists( 'HVAC_Logger' ) ) {
HVAC_Logger::error( "Schedule execution failed: " . $e->getMessage(), 'Scheduler' );
}
return false;
}
}
/**
* Handle event saved/updated
*
* @param int $event_id Event ID
*/
public function on_event_saved( $event_id ) {
$schedules = $this->schedule_manager->get_schedules_by_event( $event_id );
foreach ( $schedules as $schedule ) {
// Recalculate next run time if event date changed
$new_next_run = $this->calculate_next_run_time( $schedule );
if ( $new_next_run !== $schedule['next_run'] ) {
$this->schedule_manager->update_schedule( $schedule['schedule_id'], array(
'next_run' => $new_next_run
) );
}
}
}
/**
* Handle attendee registration
*
* @param int $attendee_id Attendee ID
* @param int $event_id Event ID
*/
public function on_attendee_registered( $attendee_id, $event_id ) {
// Process immediate registration triggers
$this->trigger_engine->process_registration_triggers( $attendee_id, $event_id );
}
/**
* Check for event date changes
*/
public function check_event_date_changes() {
// This will be called on wp hook to check for any event date changes
// and update corresponding schedules
if ( ! is_admin() || ! current_user_can( 'edit_posts' ) ) {
return;
}
// Process any date change updates
$this->trigger_engine->process_event_date_changes();
}
/**
* AJAX: Create schedule
*/
public function ajax_create_schedule() {
check_ajax_referer( 'hvac_scheduler_nonce', 'nonce' );
if ( ! is_user_logged_in() ) {
wp_send_json_error( array( 'message' => __( 'You must be logged in to create schedules.', 'hvac-community-events' ) ) );
}
$schedule_data = $this->sanitize_schedule_data( $_POST );
$schedule_data['trainer_id'] = get_current_user_id();
$result = $this->create_schedule( $schedule_data );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success( array(
'schedule_id' => $result,
'message' => __( 'Schedule created successfully.', 'hvac-community-events' )
) );
}
/**
* AJAX: Update schedule
*/
public function ajax_update_schedule() {
check_ajax_referer( 'hvac_scheduler_nonce', 'nonce' );
if ( ! is_user_logged_in() ) {
wp_send_json_error( array( 'message' => __( 'You must be logged in to update schedules.', 'hvac-community-events' ) ) );
}
$schedule_id = intval( $_POST['schedule_id'] );
$schedule_data = $this->sanitize_schedule_data( $_POST );
$result = $this->update_schedule( $schedule_id, $schedule_data );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success( array( 'message' => __( 'Schedule updated successfully.', 'hvac-community-events' ) ) );
}
/**
* AJAX: Delete schedule
*/
public function ajax_delete_schedule() {
check_ajax_referer( 'hvac_scheduler_nonce', 'nonce' );
if ( ! is_user_logged_in() ) {
wp_send_json_error( array( 'message' => __( 'You must be logged in to delete schedules.', 'hvac-community-events' ) ) );
}
$schedule_id = intval( $_POST['schedule_id'] );
$result = $this->delete_schedule( $schedule_id );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success( array( 'message' => __( 'Schedule deleted successfully.', 'hvac-community-events' ) ) );
}
/**
* AJAX: Get schedules
*/
public function ajax_get_schedules() {
check_ajax_referer( 'hvac_scheduler_nonce', 'nonce' );
if ( ! is_user_logged_in() ) {
wp_send_json_error( array( 'message' => __( 'You must be logged in to view schedules.', 'hvac-community-events' ) ) );
}
$trainer_id = get_current_user_id();
$event_id = isset( $_POST['event_id'] ) ? intval( $_POST['event_id'] ) : null;
$status_filter = isset( $_POST['status'] ) ? sanitize_text_field( $_POST['status'] ) : null;
$schedules = $this->get_trainer_schedules( $trainer_id, $event_id );
if ( $status_filter && $status_filter !== 'all' ) {
$schedules = array_filter( $schedules, function( $schedule ) use ( $status_filter ) {
return $schedule['status'] === $status_filter;
} );
}
wp_send_json_success( array( 'schedules' => array_values( $schedules ) ) );
}
/**
* AJAX: Toggle schedule status
*/
public function ajax_toggle_schedule() {
check_ajax_referer( 'hvac_scheduler_nonce', 'nonce' );
if ( ! is_user_logged_in() ) {
wp_send_json_error( array( 'message' => __( 'You must be logged in to toggle schedules.', 'hvac-community-events' ) ) );
}
$schedule_id = intval( $_POST['schedule_id'] );
$schedule = $this->schedule_manager->get_schedule( $schedule_id );
if ( ! $schedule ) {
wp_send_json_error( array( 'message' => __( 'Schedule not found.', 'hvac-community-events' ) ) );
}
$new_status = ( $schedule['status'] === 'active' ) ? 'paused' : 'active';
$result = $this->update_schedule( $schedule_id, array( 'status' => $new_status ) );
if ( is_wp_error( $result ) ) {
wp_send_json_error( array( 'message' => $result->get_error_message() ) );
}
wp_send_json_success( array(
'status' => $new_status,
'message' => sprintf( __( 'Schedule %s.', 'hvac-community-events' ), $new_status )
) );
}
/**
* AJAX: Preview recipients
*/
public function ajax_preview_recipients() {
check_ajax_referer( 'hvac_scheduler_nonce', 'nonce' );
if ( ! is_user_logged_in() ) {
wp_send_json_error( array( 'message' => __( 'You must be logged in to preview recipients.', 'hvac-community-events' ) ) );
}
$schedule_data = $this->sanitize_schedule_data( $_POST );
$schedule_data['trainer_id'] = get_current_user_id();
$recipients = $this->trigger_engine->get_schedule_recipients( $schedule_data );
wp_send_json_success( array(
'recipients' => $recipients,
'count' => count( $recipients )
) );
}
/**
* Sanitize schedule data from form input
*
* @param array $raw_data Raw POST data
* @return array Sanitized schedule data
*/
private function sanitize_schedule_data( $raw_data ) {
return array(
'schedule_name' => isset( $raw_data['schedule_name'] ) ? sanitize_text_field( $raw_data['schedule_name'] ) : '',
'event_id' => isset( $raw_data['event_id'] ) ? intval( $raw_data['event_id'] ) : null,
'template_id' => isset( $raw_data['template_id'] ) ? intval( $raw_data['template_id'] ) : 0,
'trigger_type' => isset( $raw_data['trigger_type'] ) ? sanitize_text_field( $raw_data['trigger_type'] ) : '',
'trigger_value' => isset( $raw_data['trigger_value'] ) ? intval( $raw_data['trigger_value'] ) : 0,
'trigger_unit' => isset( $raw_data['trigger_unit'] ) ? sanitize_text_field( $raw_data['trigger_unit'] ) : 'days',
'target_audience' => isset( $raw_data['target_audience'] ) ? sanitize_text_field( $raw_data['target_audience'] ) : 'all_attendees',
'custom_recipient_list' => isset( $raw_data['custom_recipient_list'] ) ? sanitize_textarea_field( $raw_data['custom_recipient_list'] ) : '',
'is_recurring' => isset( $raw_data['is_recurring'] ) ? (bool) $raw_data['is_recurring'] : false,
'recurring_interval' => isset( $raw_data['recurring_interval'] ) ? intval( $raw_data['recurring_interval'] ) : null,
'recurring_unit' => isset( $raw_data['recurring_unit'] ) ? sanitize_text_field( $raw_data['recurring_unit'] ) : null,
'max_runs' => isset( $raw_data['max_runs'] ) ? intval( $raw_data['max_runs'] ) : null,
'status' => isset( $raw_data['status'] ) ? sanitize_text_field( $raw_data['status'] ) : 'active'
);
}
/**
* Get default schedule templates
*
* @return array Default schedule configurations
*/
public function get_default_schedule_templates() {
return array(
'event_reminder_24h' => array(
'name' => __( '24-Hour Event Reminder', 'hvac-community-events' ),
'trigger_type' => 'before_event',
'trigger_value' => 1,
'trigger_unit' => 'days',
'template_category' => 'event_reminder',
'target_audience' => 'confirmed_attendees',
'description' => __( 'Send reminder 24 hours before event starts', 'hvac-community-events' )
),
'welcome_on_registration' => array(
'name' => __( 'Welcome Email on Registration', 'hvac-community-events' ),
'trigger_type' => 'on_registration',
'trigger_value' => 0,
'trigger_unit' => 'minutes',
'template_category' => 'pre_event',
'target_audience' => 'all_attendees',
'description' => __( 'Send welcome email immediately when someone registers', 'hvac-community-events' )
),
'post_event_followup' => array(
'name' => __( 'Post-Event Follow-up', 'hvac-community-events' ),
'trigger_type' => 'after_event',
'trigger_value' => 2,
'trigger_unit' => 'days',
'template_category' => 'post_event',
'target_audience' => 'all_attendees',
'description' => __( 'Send follow-up email 2 days after event', 'hvac-community-events' )
),
'certificate_notification' => array(
'name' => __( 'Certificate Ready Notification', 'hvac-community-events' ),
'trigger_type' => 'after_event',
'trigger_value' => 3,
'trigger_unit' => 'days',
'template_category' => 'certificate',
'target_audience' => 'confirmed_attendees',
'description' => __( 'Notify attendees when certificates are ready', 'hvac-community-events' )
)
);
}
}
// Initialize the scheduler
function hvac_communication_scheduler() {
return HVAC_Communication_Scheduler::instance();
}
// Initialize after plugins loaded
add_action( 'plugins_loaded', 'hvac_communication_scheduler' );

View file

@ -0,0 +1,519 @@
<?php
/**
* HVAC Community Events - Communication Trigger Engine
*
* Handles automation logic, recipient management, and email execution.
* Processes trigger conditions and manages communication delivery.
*
* @package HVAC_Community_Events
* @subpackage Communication
* @version 1.0.0
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class HVAC_Communication_Trigger_Engine
*
* Manages trigger processing and communication execution.
*/
class HVAC_Communication_Trigger_Engine {
/**
* Constructor
*/
public function __construct() {
// Initialize any required hooks or filters
}
/**
* Calculate trigger time based on event date and schedule configuration
*
* @param string $event_date Event start date (MySQL format)
* @param array $schedule Schedule configuration
* @return string|null MySQL datetime string for trigger time
*/
public function calculate_trigger_time( $event_date, $schedule ) {
if ( empty( $event_date ) || empty( $schedule['trigger_type'] ) ) {
return null;
}
$event_timestamp = strtotime( $event_date );
if ( ! $event_timestamp ) {
return null;
}
$trigger_value = intval( $schedule['trigger_value'] );
$trigger_unit = $schedule['trigger_unit'];
// Convert trigger unit to seconds
$seconds_multiplier = $this->get_unit_multiplier( $trigger_unit );
if ( ! $seconds_multiplier ) {
return null;
}
$offset_seconds = $trigger_value * $seconds_multiplier;
switch ( $schedule['trigger_type'] ) {
case 'before_event':
$trigger_timestamp = $event_timestamp - $offset_seconds;
break;
case 'after_event':
// Use event end date if available, otherwise start date
$event_end_date = get_post_meta( $schedule['event_id'], '_EventEndDate', true );
$end_timestamp = $event_end_date ? strtotime( $event_end_date ) : $event_timestamp;
$trigger_timestamp = $end_timestamp + $offset_seconds;
break;
case 'on_registration':
// Immediate trigger - return current time
return current_time( 'mysql' );
case 'custom_date':
// Custom date should be provided in schedule data
return isset( $schedule['custom_date'] ) ? $schedule['custom_date'] : null;
default:
return null;
}
// Ensure trigger time is in the future
if ( $trigger_timestamp <= time() ) {
return null;
}
return date( 'Y-m-d H:i:s', $trigger_timestamp );
}
/**
* Get unit multiplier for converting to seconds
*
* @param string $unit Time unit
* @return int|false Multiplier or false if invalid
*/
private function get_unit_multiplier( $unit ) {
$multipliers = array(
'minutes' => MINUTE_IN_SECONDS,
'hours' => HOUR_IN_SECONDS,
'days' => DAY_IN_SECONDS,
'weeks' => WEEK_IN_SECONDS
);
return isset( $multipliers[$unit] ) ? $multipliers[$unit] : false;
}
/**
* Get recipients for a schedule based on target audience settings
*
* @param array $schedule Schedule configuration
* @return array Array of recipient data
*/
public function get_schedule_recipients( $schedule ) {
$recipients = array();
switch ( $schedule['target_audience'] ) {
case 'all_attendees':
$recipients = $this->get_all_event_attendees( $schedule['event_id'] );
break;
case 'confirmed_attendees':
$recipients = $this->get_confirmed_attendees( $schedule['event_id'] );
break;
case 'pending_attendees':
$recipients = $this->get_pending_attendees( $schedule['event_id'] );
break;
case 'custom_list':
$recipients = $this->parse_custom_recipient_list( $schedule['custom_recipient_list'] );
break;
}
// Apply additional conditions if specified
if ( ! empty( $schedule['conditions'] ) ) {
$recipients = $this->apply_recipient_conditions( $recipients, $schedule['conditions'] );
}
return $recipients;
}
/**
* Get all attendees for an event
*
* @param int $event_id Event ID
* @return array Array of attendee data
*/
private function get_all_event_attendees( $event_id ) {
if ( empty( $event_id ) ) {
return array();
}
// Use the Email Attendees Data class for consistent attendee retrieval
$email_data = new HVAC_Email_Attendees_Data( $event_id );
$attendees = $email_data->get_attendees();
$recipients = array();
foreach ( $attendees as $attendee ) {
$recipients[] = array(
'email' => $attendee['email'],
'name' => $attendee['name'],
'attendee_id' => $attendee['attendee_id'],
'ticket_name' => $attendee['ticket_name'],
'status' => 'confirmed' // Default status
);
}
return $recipients;
}
/**
* Get confirmed attendees only
*
* @param int $event_id Event ID
* @return array Array of confirmed attendee data
*/
private function get_confirmed_attendees( $event_id ) {
$all_attendees = $this->get_all_event_attendees( $event_id );
// For now, treat all attendees as confirmed
// This can be enhanced later based on ticket status if needed
return array_filter( $all_attendees, function( $attendee ) {
return $attendee['status'] === 'confirmed';
});
}
/**
* Get pending attendees only
*
* @param int $event_id Event ID
* @return array Array of pending attendee data
*/
private function get_pending_attendees( $event_id ) {
$all_attendees = $this->get_all_event_attendees( $event_id );
return array_filter( $all_attendees, function( $attendee ) {
return $attendee['status'] === 'pending';
});
}
/**
* Parse custom recipient list from text input
*
* @param string $recipient_list Comma or line-separated email list
* @return array Array of recipient data
*/
private function parse_custom_recipient_list( $recipient_list ) {
if ( empty( $recipient_list ) ) {
return array();
}
$recipients = array();
$lines = preg_split( '/[\r\n,]+/', $recipient_list );
foreach ( $lines as $line ) {
$line = trim( $line );
if ( empty( $line ) ) {
continue;
}
// Check if line contains both name and email
if ( preg_match( '/(.+?)\s*<(.+?)>/', $line, $matches ) ) {
$name = trim( $matches[1] );
$email = trim( $matches[2] );
} else {
// Just email address
$email = $line;
$name = '';
}
if ( is_email( $email ) ) {
$recipients[] = array(
'email' => $email,
'name' => $name,
'attendee_id' => 0,
'ticket_name' => '',
'status' => 'custom'
);
}
}
return $recipients;
}
/**
* Apply additional conditions to filter recipients
*
* @param array $recipients Current recipient list
* @param array $conditions Filter conditions
* @return array Filtered recipients
*/
private function apply_recipient_conditions( $recipients, $conditions ) {
if ( empty( $conditions ) ) {
return $recipients;
}
foreach ( $conditions as $condition ) {
switch ( $condition['type'] ) {
case 'ticket_type':
$recipients = array_filter( $recipients, function( $recipient ) use ( $condition ) {
return $recipient['ticket_name'] === $condition['value'];
});
break;
case 'exclude_emails':
$exclude_list = array_map( 'trim', explode( ',', $condition['value'] ) );
$recipients = array_filter( $recipients, function( $recipient ) use ( $exclude_list ) {
return ! in_array( $recipient['email'], $exclude_list );
});
break;
}
}
return $recipients;
}
/**
* Execute communication for a schedule
*
* @param array $schedule Schedule configuration
* @param array $recipients Recipients to send to
* @return bool Success status
*/
public function execute_communication( $schedule, $recipients ) {
if ( empty( $recipients ) || empty( $schedule['template_id'] ) ) {
return false;
}
// Get the email template
$template = get_post( $schedule['template_id'] );
if ( ! $template || $template->post_type !== 'hvac_email_template' ) {
return false;
}
$subject = $template->post_title;
$message = $template->post_content;
// Get event details for placeholder replacement
$event_details = null;
if ( ! empty( $schedule['event_id'] ) ) {
$email_data = new HVAC_Email_Attendees_Data( $schedule['event_id'] );
$event_details = $email_data->get_event_details();
}
$success_count = 0;
$total_count = count( $recipients );
foreach ( $recipients as $recipient ) {
// Replace placeholders in subject and message
$personalized_subject = $this->replace_placeholders( $subject, $recipient, $event_details );
$personalized_message = $this->replace_placeholders( $message, $recipient, $event_details );
// Send email
$headers = array(
'Content-Type: text/html; charset=UTF-8'
);
// Add sender information
$trainer = get_user_by( 'id', $schedule['trainer_id'] );
if ( $trainer ) {
$from_name = $trainer->display_name;
$from_email = $trainer->user_email;
// Check for trainer business name
$business_name = get_user_meta( $trainer->ID, 'business_name', true );
if ( ! empty( $business_name ) ) {
$from_name = $business_name;
}
$headers[] = 'From: ' . $from_name . ' <' . $from_email . '>';
}
$mail_sent = wp_mail( $recipient['email'], $personalized_subject, wpautop( $personalized_message ), $headers );
if ( $mail_sent ) {
$success_count++;
}
// Log individual send attempt if logger is available
if ( class_exists( 'HVAC_Logger' ) ) {
$status = $mail_sent ? 'sent' : 'failed';
HVAC_Logger::info( "Email {$status} to {$recipient['email']} for schedule {$schedule['schedule_id']}", 'Communication Engine' );
}
}
return $success_count === $total_count;
}
/**
* Replace placeholders in email content
*
* @param string $content Email subject or content
* @param array $recipient Recipient data
* @param array|null $event_details Event details for placeholders
* @return string Content with placeholders replaced
*/
private function replace_placeholders( $content, $recipient, $event_details = null ) {
$placeholders = array(
'{attendee_name}' => $recipient['name'],
'{attendee_email}' => $recipient['email'],
'{ticket_type}' => $recipient['ticket_name']
);
if ( $event_details ) {
$placeholders['{event_title}'] = $event_details['title'];
$placeholders['{event_date}'] = $event_details['start_date'];
$placeholders['{event_time}'] = $event_details['start_time'];
$placeholders['{event_start_date}'] = $event_details['start_date'];
$placeholders['{event_start_time}'] = $event_details['start_time'];
$placeholders['{event_end_date}'] = $event_details['end_date'];
$placeholders['{event_end_time}'] = $event_details['end_time'];
}
// Add current date/time placeholders
$placeholders['{current_date}'] = date( 'F j, Y' );
$placeholders['{current_time}'] = date( 'g:i a' );
$placeholders['{current_year}'] = date( 'Y' );
return str_replace( array_keys( $placeholders ), array_values( $placeholders ), $content );
}
/**
* Process registration-triggered communications
*
* @param int $attendee_id Attendee ID
* @param int $event_id Event ID
*/
public function process_registration_triggers( $attendee_id, $event_id ) {
global $wpdb;
// Get all active schedules with registration triggers for this event
$schedules_table = $wpdb->prefix . 'hvac_communication_schedules';
$schedules = $wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$schedules_table}
WHERE event_id = %d
AND trigger_type = 'on_registration'
AND status = 'active'",
$event_id
), ARRAY_A );
foreach ( $schedules as $schedule ) {
// Get attendee details
$attendee_post = get_post( $attendee_id );
if ( ! $attendee_post ) {
continue;
}
$attendee_email = get_post_meta( $attendee_id, '_tribe_tickets_email', true );
if ( empty( $attendee_email ) ) {
$attendee_email = get_post_meta( $attendee_id, '_tribe_tpp_email', true );
}
$attendee_name = get_post_meta( $attendee_id, '_tribe_tickets_full_name', true );
if ( empty( $attendee_name ) ) {
$attendee_name = get_post_meta( $attendee_id, '_tribe_tpp_full_name', true );
}
if ( empty( $attendee_email ) || ! is_email( $attendee_email ) ) {
continue;
}
// Create recipient array
$recipients = array(
array(
'email' => $attendee_email,
'name' => $attendee_name,
'attendee_id' => $attendee_id,
'ticket_name' => '',
'status' => 'confirmed'
)
);
// Execute communication
$this->execute_communication( $schedule, $recipients );
// Update schedule run tracking
$schedule_manager = new HVAC_Communication_Schedule_Manager();
$schedule_manager->update_schedule_run_tracking( $schedule['schedule_id'] );
}
}
/**
* Process event date changes and update affected schedules
*/
public function process_event_date_changes() {
global $wpdb;
// This would be called when event dates are updated
// For now, it's a placeholder for future implementation
if ( class_exists( 'HVAC_Logger' ) ) {
HVAC_Logger::info( 'Processing event date changes', 'Communication Engine' );
}
}
/**
* Validate recipients against event attendees
*
* @param array $recipients Recipients to validate
* @param int $event_id Event ID
* @return array Valid recipients only
*/
public function validate_recipients( $recipients, $event_id = null ) {
if ( empty( $recipients ) ) {
return array();
}
$valid_recipients = array();
foreach ( $recipients as $recipient ) {
// Basic email validation
if ( empty( $recipient['email'] ) || ! is_email( $recipient['email'] ) ) {
continue;
}
// If event ID provided, verify recipient is actually an attendee
if ( $event_id ) {
$all_attendees = $this->get_all_event_attendees( $event_id );
$is_attendee = false;
foreach ( $all_attendees as $attendee ) {
if ( $attendee['email'] === $recipient['email'] ) {
$is_attendee = true;
break;
}
}
if ( ! $is_attendee && $recipient['status'] !== 'custom' ) {
continue;
}
}
$valid_recipients[] = $recipient;
}
return $valid_recipients;
}
/**
* Get communication statistics for a schedule
*
* @param int $schedule_id Schedule ID
* @return array Statistics array
*/
public function get_schedule_statistics( $schedule_id ) {
global $wpdb;
$logs_table = $wpdb->prefix . 'hvac_communication_logs';
$stats = $wpdb->get_row( $wpdb->prepare(
"SELECT
COUNT(*) as total_sends,
COUNT(CASE WHEN status = 'sent' THEN 1 END) as successful_sends,
COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed_sends,
MAX(sent_date) as last_sent
FROM {$logs_table}
WHERE schedule_id = %d",
$schedule_id
), ARRAY_A );
return $stats;
}
}

View file

@ -0,0 +1,832 @@
<?php
/**
* Communication Schedules Template
*
* Template for managing automated communication schedules.
*
* @package HVAC_Community_Events
* @subpackage Templates/Communication
* @version 1.0.0
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Get current user
$current_user = wp_get_current_user();
$trainer_id = $current_user->ID;
// Initialize classes
if ( ! class_exists( 'HVAC_Communication_Scheduler' ) ) {
require_once HVAC_CE_PLUGIN_DIR . 'includes/communication/class-communication-scheduler.php';
}
if ( ! class_exists( 'HVAC_Communication_Schedule_Manager' ) ) {
require_once HVAC_CE_PLUGIN_DIR . 'includes/communication/class-communication-schedule-manager.php';
}
if ( ! class_exists( 'HVAC_Communication_Templates' ) ) {
require_once HVAC_CE_PLUGIN_DIR . 'includes/communication/class-communication-templates.php';
}
$scheduler = HVAC_Communication_Scheduler::instance();
$schedule_manager = new HVAC_Communication_Schedule_Manager();
$templates_manager = new HVAC_Communication_Templates();
// Get user's schedules
$schedules = $scheduler->get_trainer_schedules( $trainer_id );
// Get user's templates for dropdown
$templates = $templates_manager->get_user_templates( $trainer_id );
// Get user's events for dropdown
$events_query = new WP_Query( array(
'post_type' => 'tribe_events',
'author' => $trainer_id,
'posts_per_page' => -1,
'post_status' => array( 'publish', 'future', 'draft' )
) );
$user_events = $events_query->posts;
?>
<div class="hvac-communication-schedules">
<header class="page-header">
<h1>Communication Schedules</h1>
<p>Create and manage automated email schedules for your events.</p>
</header>
<div class="schedules-content">
<!-- Create New Schedule Section -->
<section class="create-schedule-section">
<h2>Create New Schedule</h2>
<form id="create-schedule-form" class="schedule-form">
<?php wp_nonce_field( 'hvac_scheduler_nonce', 'nonce' ); ?>
<div class="form-row">
<div class="form-group">
<label for="schedule_name">Schedule Name</label>
<input type="text" id="schedule_name" name="schedule_name" placeholder="e.g., 24h Event Reminder" required>
</div>
<div class="form-group">
<label for="template_id">Email Template</label>
<select id="template_id" name="template_id" required>
<option value="">Select a template</option>
<?php foreach ( $templates as $template ) : ?>
<option value="<?php echo esc_attr( $template['id'] ); ?>">
<?php echo esc_html( $template['title'] ); ?>
</option>
<?php endforeach; ?>
</select>
<small>Don't have templates? <a href="/communication-templates/" target="_blank">Create one here</a></small>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="event_id">Event (Optional)</label>
<select id="event_id" name="event_id">
<option value="">All Events (Global Schedule)</option>
<?php foreach ( $user_events as $event ) : ?>
<option value="<?php echo esc_attr( $event->ID ); ?>">
<?php echo esc_html( $event->post_title ); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="target_audience">Target Audience</label>
<select id="target_audience" name="target_audience" required>
<option value="all_attendees">All Attendees</option>
<option value="confirmed_attendees">Confirmed Attendees</option>
<option value="pending_attendees">Pending Attendees</option>
<option value="custom_list">Custom Email List</option>
</select>
</div>
</div>
<div class="form-group custom-recipient-group" style="display: none;">
<label for="custom_recipient_list">Custom Recipients</label>
<textarea id="custom_recipient_list" name="custom_recipient_list"
placeholder="Enter email addresses separated by commas or line breaks:&#10;john@example.com&#10;Jane Smith &lt;jane@example.com&gt;&#10;trainer@company.com"></textarea>
<small>Enter one email per line or separate with commas. Format: email@domain.com or Name &lt;email@domain.com&gt;</small>
</div>
<fieldset class="trigger-settings">
<legend>Trigger Settings</legend>
<div class="form-row">
<div class="form-group">
<label for="trigger_type">Trigger Type</label>
<select id="trigger_type" name="trigger_type" required>
<option value="before_event">Before Event</option>
<option value="after_event">After Event</option>
<option value="on_registration">On Registration</option>
<option value="custom_date">Custom Date</option>
</select>
</div>
<div class="form-group timing-group">
<label for="trigger_value">Timing</label>
<div class="timing-inputs">
<input type="number" id="trigger_value" name="trigger_value" min="0" value="1" required>
<select id="trigger_unit" name="trigger_unit" required>
<option value="minutes">Minutes</option>
<option value="hours">Hours</option>
<option value="days" selected>Days</option>
<option value="weeks">Weeks</option>
</select>
</div>
</div>
</div>
<div class="form-group custom-date-group" style="display: none;">
<label for="custom_date">Custom Date & Time</label>
<input type="datetime-local" id="custom_date" name="custom_date">
</div>
</fieldset>
<fieldset class="recurring-settings">
<legend>Recurring Options (Optional)</legend>
<div class="form-group">
<label>
<input type="checkbox" id="is_recurring" name="is_recurring" value="1">
Make this a recurring schedule
</label>
</div>
<div class="recurring-options" style="display: none;">
<div class="form-row">
<div class="form-group">
<label for="recurring_interval">Repeat Every</label>
<input type="number" id="recurring_interval" name="recurring_interval" min="1" value="1">
</div>
<div class="form-group">
<label for="recurring_unit">Unit</label>
<select id="recurring_unit" name="recurring_unit">
<option value="days">Days</option>
<option value="weeks">Weeks</option>
<option value="months">Months</option>
</select>
</div>
<div class="form-group">
<label for="max_runs">Max Runs (Optional)</label>
<input type="number" id="max_runs" name="max_runs" min="1" placeholder="Unlimited">
</div>
</div>
</div>
</fieldset>
<div class="form-actions">
<button type="button" id="preview-recipients-btn" class="btn btn-secondary">Preview Recipients</button>
<button type="submit" class="btn btn-primary">Create Schedule</button>
</div>
</form>
</section>
<!-- Existing Schedules Section -->
<section class="existing-schedules-section">
<h2>Your Schedules</h2>
<?php if ( empty( $schedules ) ) : ?>
<div class="no-schedules">
<p>You haven't created any communication schedules yet.</p>
<p>Use the form above to create your first automated email schedule.</p>
</div>
<?php else : ?>
<div class="schedules-filters">
<select id="status-filter">
<option value="all">All Statuses</option>
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="completed">Completed</option>
</select>
<select id="event-filter">
<option value="">All Events</option>
<?php foreach ( $user_events as $event ) : ?>
<option value="<?php echo esc_attr( $event->ID ); ?>">
<?php echo esc_html( $event->post_title ); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="schedules-table-container">
<table class="schedules-table">
<thead>
<tr>
<th>Schedule Name</th>
<th>Event</th>
<th>Template</th>
<th>Trigger</th>
<th>Status</th>
<th>Next Run</th>
<th>Runs</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="schedules-table-body">
<?php foreach ( $schedules as $schedule ) : ?>
<tr data-schedule-id="<?php echo esc_attr( $schedule['schedule_id'] ); ?>"
data-status="<?php echo esc_attr( $schedule['status'] ); ?>"
data-event="<?php echo esc_attr( $schedule['event_id'] ); ?>">
<td>
<strong><?php echo esc_html( $schedule['schedule_name'] ?: 'Unnamed Schedule' ); ?></strong>
<small><?php echo esc_html( $schedule['target_audience'] ); ?></small>
</td>
<td>
<?php if ( $schedule['event_name'] ) : ?>
<?php echo esc_html( $schedule['event_name'] ); ?>
<?php else : ?>
<em>All Events</em>
<?php endif; ?>
</td>
<td><?php echo esc_html( $schedule['template_name'] ?: 'Unknown Template' ); ?></td>
<td>
<?php
$trigger_text = ucfirst( str_replace( '_', ' ', $schedule['trigger_type'] ) );
if ( in_array( $schedule['trigger_type'], array( 'before_event', 'after_event' ) ) ) {
$trigger_text .= ' (' . $schedule['trigger_value'] . ' ' . $schedule['trigger_unit'] . ')';
}
echo esc_html( $trigger_text );
?>
</td>
<td>
<span class="status-badge status-<?php echo esc_attr( $schedule['status'] ); ?>">
<?php echo esc_html( ucfirst( $schedule['status'] ) ); ?>
</span>
</td>
<td>
<?php if ( $schedule['next_run'] ) : ?>
<?php echo esc_html( date( 'M j, Y g:i a', strtotime( $schedule['next_run'] ) ) ); ?>
<?php else : ?>
<em>N/A</em>
<?php endif; ?>
</td>
<td>
<?php echo esc_html( $schedule['run_count'] ); ?>
<?php if ( $schedule['max_runs'] ) : ?>
/ <?php echo esc_html( $schedule['max_runs'] ); ?>
<?php endif; ?>
</td>
<td class="actions">
<button class="btn-toggle-schedule"
data-schedule-id="<?php echo esc_attr( $schedule['schedule_id'] ); ?>"
data-current-status="<?php echo esc_attr( $schedule['status'] ); ?>">
<?php echo $schedule['status'] === 'active' ? 'Pause' : 'Activate'; ?>
</button>
<button class="btn-edit-schedule"
data-schedule-id="<?php echo esc_attr( $schedule['schedule_id'] ); ?>">
Edit
</button>
<button class="btn-delete-schedule"
data-schedule-id="<?php echo esc_attr( $schedule['schedule_id'] ); ?>">
Delete
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</section>
<!-- Schedule Default Templates -->
<section class="schedule-templates-section">
<h2>Quick Start Templates</h2>
<p>Use these pre-configured schedule templates to get started quickly.</p>
<div class="template-grid">
<?php
$default_templates = $scheduler->get_default_schedule_templates();
foreach ( $default_templates as $template_key => $template ) :
?>
<div class="template-card" data-template="<?php echo esc_attr( $template_key ); ?>">
<h3><?php echo esc_html( $template['name'] ); ?></h3>
<p><?php echo esc_html( $template['description'] ); ?></p>
<div class="template-details">
<span class="trigger-type"><?php echo esc_html( ucfirst( str_replace( '_', ' ', $template['trigger_type'] ) ) ); ?></span>
<span class="timing"><?php echo esc_html( $template['trigger_value'] . ' ' . $template['trigger_unit'] ); ?></span>
</div>
<button class="btn btn-outline use-template-btn"
data-template="<?php echo esc_attr( $template_key ); ?>">
Use This Template
</button>
</div>
<?php endforeach; ?>
</div>
</section>
</div>
<!-- Preview Recipients Modal -->
<div id="recipients-preview-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>Recipients Preview</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div id="recipients-preview-content">
<!-- Preview content will be loaded here -->
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary modal-close">Close</button>
</div>
</div>
</div>
</div>
<style>
.hvac-communication-schedules {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.page-header {
text-align: center;
margin-bottom: 40px;
}
.page-header h1 {
color: #2c3e50;
margin-bottom: 10px;
}
.schedule-form {
background: #f8f9fa;
padding: 30px;
border-radius: 8px;
margin-bottom: 40px;
border: 1px solid #e9ecef;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group label {
font-weight: 600;
margin-bottom: 5px;
color: #495057;
}
.form-group input,
.form-group select,
.form-group textarea {
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 14px;
}
.form-group small {
color: #6c757d;
margin-top: 5px;
}
.timing-inputs {
display: flex;
gap: 10px;
}
.timing-inputs input {
flex: 1;
}
.timing-inputs select {
flex: 2;
}
fieldset {
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 20px;
margin-bottom: 20px;
}
fieldset legend {
font-weight: 600;
color: #495057;
padding: 0 10px;
}
.form-actions {
display: flex;
gap: 15px;
justify-content: flex-end;
margin-top: 30px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn-primary {
background: #007cba;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-outline {
background: transparent;
border: 1px solid #007cba;
color: #007cba;
}
.schedules-filters {
display: flex;
gap: 15px;
margin-bottom: 20px;
}
.schedules-table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.schedules-table th,
.schedules-table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #e9ecef;
}
.schedules-table th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
}
.status-badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
.status-active {
background: #d4edda;
color: #155724;
}
.status-paused {
background: #fff3cd;
color: #856404;
}
.status-completed {
background: #d1ecf1;
color: #0c5460;
}
.actions {
white-space: nowrap;
}
.actions button {
font-size: 12px;
padding: 4px 8px;
margin-right: 5px;
border: 1px solid #ccc;
background: white;
cursor: pointer;
border-radius: 3px;
}
.template-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.template-card {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
text-align: center;
}
.template-details {
display: flex;
justify-content: center;
gap: 15px;
margin: 15px 0;
font-size: 14px;
color: #6c757d;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
border-radius: 8px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #e9ecef;
text-align: right;
}
.modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
}
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
.timing-inputs {
flex-direction: column;
}
.schedules-table-container {
overflow-x: auto;
}
.template-grid {
grid-template-columns: 1fr;
}
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Form elements
const form = document.getElementById('create-schedule-form');
const targetAudienceSelect = document.getElementById('target_audience');
const customRecipientGroup = document.querySelector('.custom-recipient-group');
const triggerTypeSelect = document.getElementById('trigger_type');
const timingGroup = document.querySelector('.timing-group');
const customDateGroup = document.querySelector('.custom-date-group');
const isRecurringCheckbox = document.getElementById('is_recurring');
const recurringOptions = document.querySelector('.recurring-options');
const previewBtn = document.getElementById('preview-recipients-btn');
const modal = document.getElementById('recipients-preview-modal');
// Show/hide custom recipient list
targetAudienceSelect.addEventListener('change', function() {
if (this.value === 'custom_list') {
customRecipientGroup.style.display = 'block';
} else {
customRecipientGroup.style.display = 'none';
}
});
// Show/hide timing controls based on trigger type
triggerTypeSelect.addEventListener('change', function() {
if (this.value === 'custom_date') {
timingGroup.style.display = 'none';
customDateGroup.style.display = 'block';
} else if (this.value === 'on_registration') {
timingGroup.style.display = 'none';
customDateGroup.style.display = 'none';
} else {
timingGroup.style.display = 'block';
customDateGroup.style.display = 'none';
}
});
// Show/hide recurring options
isRecurringCheckbox.addEventListener('change', function() {
if (this.checked) {
recurringOptions.style.display = 'block';
} else {
recurringOptions.style.display = 'none';
}
});
// Modal controls
document.querySelectorAll('.modal-close').forEach(button => {
button.addEventListener('click', function() {
modal.style.display = 'none';
});
});
// Close modal on outside click
modal.addEventListener('click', function(e) {
if (e.target === modal) {
modal.style.display = 'none';
}
});
// Preview recipients
previewBtn.addEventListener('click', function() {
const formData = new FormData(form);
fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
const content = document.getElementById('recipients-preview-content');
content.innerHTML = `
<h4>Found ${data.data.count} recipients:</h4>
<ul>
${data.data.recipients.map(r => `<li>${r.name} &lt;${r.email}&gt;</li>`).join('')}
</ul>
`;
modal.style.display = 'flex';
} else {
alert('Error: ' + data.data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error previewing recipients');
});
});
// Form submission
form.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(form);
formData.append('action', 'hvac_create_schedule');
fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Schedule created successfully!');
location.reload();
} else {
alert('Error: ' + data.data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error creating schedule');
});
});
// Schedule actions
document.addEventListener('click', function(e) {
if (e.target.classList.contains('btn-toggle-schedule')) {
const scheduleId = e.target.dataset.scheduleId;
const formData = new FormData();
formData.append('action', 'hvac_toggle_schedule');
formData.append('schedule_id', scheduleId);
formData.append('nonce', '<?php echo wp_create_nonce('hvac_scheduler_nonce'); ?>');
fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Error: ' + data.data.message);
}
});
}
if (e.target.classList.contains('btn-delete-schedule')) {
if (confirm('Are you sure you want to delete this schedule?')) {
const scheduleId = e.target.dataset.scheduleId;
const formData = new FormData();
formData.append('action', 'hvac_delete_schedule');
formData.append('schedule_id', scheduleId);
formData.append('nonce', '<?php echo wp_create_nonce('hvac_scheduler_nonce'); ?>');
fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Error: ' + data.data.message);
}
});
}
}
if (e.target.classList.contains('use-template-btn')) {
const templateKey = e.target.dataset.template;
const template = <?php echo json_encode( $default_templates ); ?>[templateKey];
// Fill form with template values
document.getElementById('trigger_type').value = template.trigger_type;
document.getElementById('trigger_value').value = template.trigger_value;
document.getElementById('trigger_unit').value = template.trigger_unit;
document.getElementById('target_audience').value = template.target_audience;
// Trigger change events
triggerTypeSelect.dispatchEvent(new Event('change'));
targetAudienceSelect.dispatchEvent(new Event('change'));
// Scroll to form
form.scrollIntoView({ behavior: 'smooth' });
}
});
// Filters
const statusFilter = document.getElementById('status-filter');
const eventFilter = document.getElementById('event-filter');
function applyFilters() {
const statusValue = statusFilter ? statusFilter.value : 'all';
const eventValue = eventFilter ? eventFilter.value : '';
document.querySelectorAll('#schedules-table-body tr').forEach(row => {
let show = true;
if (statusValue !== 'all' && row.dataset.status !== statusValue) {
show = false;
}
if (eventValue !== '' && row.dataset.event !== eventValue) {
show = false;
}
row.style.display = show ? '' : 'none';
});
}
if (statusFilter) statusFilter.addEventListener('change', applyFilters);
if (eventFilter) eventFilter.addEventListener('change', applyFilters);
});
</script>