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:
parent
83f9285926
commit
a0d47b3b3e
15 changed files with 4629 additions and 2 deletions
|
|
@ -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');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
68
wordpress-dev/tests/e2e/debug-button-click.test.ts
Normal file
68
wordpress-dev/tests/e2e/debug-button-click.test.ts
Normal 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');
|
||||||
|
});
|
||||||
54
wordpress-dev/tests/e2e/debug-button-simple.test.ts
Normal file
54
wordpress-dev/tests/e2e/debug-button-simple.test.ts
Normal 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');
|
||||||
|
});
|
||||||
66
wordpress-dev/tests/e2e/debug-modal.test.ts
Normal file
66
wordpress-dev/tests/e2e/debug-modal.test.ts
Normal 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');
|
||||||
|
});
|
||||||
38
wordpress-dev/tests/e2e/debug-scripts.test.ts
Normal file
38
wordpress-dev/tests/e2e/debug-scripts.test.ts
Normal 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');
|
||||||
|
});
|
||||||
332
wordpress-dev/tests/e2e/debug-templates.test.ts
Normal file
332
wordpress-dev/tests/e2e/debug-templates.test.ts
Normal 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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
51
wordpress-dev/tests/e2e/simple-modal-test.test.ts
Normal file
51
wordpress-dev/tests/e2e/simple-modal-test.test.ts
Normal 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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -99,6 +99,14 @@ function hvac_ce_create_required_pages() {
|
||||||
'title' => 'Google Sheets Integration',
|
'title' => 'Google Sheets Integration',
|
||||||
'content' => '<!-- wp:shortcode -->[hvac_google_sheets]<!-- /wp:shortcode -->',
|
'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.
|
// REMOVED: 'submit-event' page creation. Will link to default TEC CE page.
|
||||||
// 'submit-event' => [
|
// 'submit-event' => [
|
||||||
// 'title' => 'Submit Event',
|
// 'title' => 'Submit Event',
|
||||||
|
|
@ -253,7 +261,7 @@ function hvac_ce_enqueue_common_assets() {
|
||||||
'hvac-dashboard', 'community-login', 'trainer-registration', 'trainer-profile',
|
'hvac-dashboard', 'community-login', 'trainer-registration', 'trainer-profile',
|
||||||
'manage-event', 'event-summary', 'email-attendees', 'certificate-reports',
|
'manage-event', 'event-summary', 'email-attendees', 'certificate-reports',
|
||||||
'generate-certificates', 'certificate-fix', 'hvac-documentation', 'attendee-profile',
|
'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
|
// Only proceed if we're on an HVAC page
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,12 @@ class HVAC_Community_Events {
|
||||||
'certificates/test-rewrite-rules.php', // Rewrite rules testing (temporary)
|
'certificates/test-rewrite-rules.php', // Rewrite rules testing (temporary)
|
||||||
'google-sheets/class-google-sheets-auth.php', // Google Sheets authentication
|
'google-sheets/class-google-sheets-auth.php', // Google Sheets authentication
|
||||||
'google-sheets/class-google-sheets-manager.php', // Google Sheets management
|
'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
|
// Make sure Login_Handler is loaded first for shortcode registration
|
||||||
$login_handler_path = HVAC_CE_PLUGIN_DIR . 'includes/community/class-login-handler.php';
|
$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')) {
|
if (class_exists('HVAC_Help_System')) {
|
||||||
HVAC_Help_System::instance();
|
HVAC_Help_System::instance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize communication system
|
||||||
|
$this->init_communication_system();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -390,6 +398,9 @@ class HVAC_Community_Events {
|
||||||
// Add communication templates shortcode
|
// Add communication templates shortcode
|
||||||
add_shortcode('hvac_communication_templates', array($this, 'render_communication_templates'));
|
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
|
// Removed shortcode override - let The Events Calendar Community Events handle this shortcode
|
||||||
// add_shortcode('tribe_community_events', array($this, 'render_tribe_community_events'));
|
// 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
|
* Render communication templates content
|
||||||
*/
|
*/
|
||||||
|
|
@ -812,4 +842,28 @@ class HVAC_Community_Events {
|
||||||
return ob_get_clean();
|
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
|
} // End class HVAC_Community_Events
|
||||||
|
|
@ -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' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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' );
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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: john@example.com Jane Smith <jane@example.com> trainer@company.com"></textarea>
|
||||||
|
<small>Enter one email per line or separate with commas. Format: email@domain.com or Name <email@domain.com></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">×</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} <${r.email}></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>
|
||||||
Loading…
Reference in a new issue