From a0d47b3b3e5cea85ca2c719e9c13467ebb28b191 Mon Sep 17 00:00:00 2001 From: bengizmo Date: Sat, 14 Jun 2025 00:07:30 -0300 Subject: [PATCH] feat: Implement configurable Communication Schedule system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ...communication-templates-validation.test.ts | 556 ++++++++++++ .../tests/e2e/debug-button-click.test.ts | 68 ++ .../tests/e2e/debug-button-simple.test.ts | 54 ++ wordpress-dev/tests/e2e/debug-modal.test.ts | 66 ++ wordpress-dev/tests/e2e/debug-scripts.test.ts | 38 + .../tests/e2e/debug-templates.test.ts | 332 +++++++ .../tests/e2e/simple-modal-test.test.ts | 51 ++ .../hvac-community-events.php | 10 +- .../includes/class-hvac-community-events.php | 56 +- .../class-communication-installer.php | 383 ++++++++ .../class-communication-logger.php | 467 ++++++++++ .../class-communication-schedule-manager.php | 603 +++++++++++++ .../class-communication-scheduler.php | 596 +++++++++++++ .../class-communication-trigger-engine.php | 519 +++++++++++ .../template-communication-schedules.php | 832 ++++++++++++++++++ 15 files changed, 4629 insertions(+), 2 deletions(-) create mode 100644 wordpress-dev/tests/e2e/communication-templates-validation.test.ts create mode 100644 wordpress-dev/tests/e2e/debug-button-click.test.ts create mode 100644 wordpress-dev/tests/e2e/debug-button-simple.test.ts create mode 100644 wordpress-dev/tests/e2e/debug-modal.test.ts create mode 100644 wordpress-dev/tests/e2e/debug-scripts.test.ts create mode 100644 wordpress-dev/tests/e2e/debug-templates.test.ts create mode 100644 wordpress-dev/tests/e2e/simple-modal-test.test.ts create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-installer.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-logger.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-schedule-manager.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-scheduler.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-trigger-engine.php create mode 100644 wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/communication/template-communication-schedules.php diff --git a/wordpress-dev/tests/e2e/communication-templates-validation.test.ts b/wordpress-dev/tests/e2e/communication-templates-validation.test.ts new file mode 100644 index 00000000..dd3e662a --- /dev/null +++ b/wordpress-dev/tests/e2e/communication-templates-validation.test.ts @@ -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'); + }); + +}); \ No newline at end of file diff --git a/wordpress-dev/tests/e2e/debug-button-click.test.ts b/wordpress-dev/tests/e2e/debug-button-click.test.ts new file mode 100644 index 00000000..a32c4398 --- /dev/null +++ b/wordpress-dev/tests/e2e/debug-button-click.test.ts @@ -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'); +}); \ No newline at end of file diff --git a/wordpress-dev/tests/e2e/debug-button-simple.test.ts b/wordpress-dev/tests/e2e/debug-button-simple.test.ts new file mode 100644 index 00000000..c7da31b5 --- /dev/null +++ b/wordpress-dev/tests/e2e/debug-button-simple.test.ts @@ -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'); +}); \ No newline at end of file diff --git a/wordpress-dev/tests/e2e/debug-modal.test.ts b/wordpress-dev/tests/e2e/debug-modal.test.ts new file mode 100644 index 00000000..c3ddbef7 --- /dev/null +++ b/wordpress-dev/tests/e2e/debug-modal.test.ts @@ -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'); +}); \ No newline at end of file diff --git a/wordpress-dev/tests/e2e/debug-scripts.test.ts b/wordpress-dev/tests/e2e/debug-scripts.test.ts new file mode 100644 index 00000000..ff8789d3 --- /dev/null +++ b/wordpress-dev/tests/e2e/debug-scripts.test.ts @@ -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'); +}); \ No newline at end of file diff --git a/wordpress-dev/tests/e2e/debug-templates.test.ts b/wordpress-dev/tests/e2e/debug-templates.test.ts new file mode 100644 index 00000000..4a2d4aaf --- /dev/null +++ b/wordpress-dev/tests/e2e/debug-templates.test.ts @@ -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)); + } + }); + +}); \ No newline at end of file diff --git a/wordpress-dev/tests/e2e/simple-modal-test.test.ts b/wordpress-dev/tests/e2e/simple-modal-test.test.ts new file mode 100644 index 00000000..91b40227 --- /dev/null +++ b/wordpress-dev/tests/e2e/simple-modal-test.test.ts @@ -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'); + } +}); \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php index 67242cc3..4944098d 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/hvac-community-events.php @@ -99,6 +99,14 @@ function hvac_ce_create_required_pages() { 'title' => 'Google Sheets Integration', 'content' => '[hvac_google_sheets]', ], + 'communication-templates' => [ // Add Communication Templates page + 'title' => 'Communication Templates', + 'content' => '[hvac_communication_templates]', + ], + 'communication-schedules' => [ // Add Communication Schedules page + 'title' => 'Communication Schedules', + 'content' => '[hvac_communication_schedules]', + ], // REMOVED: 'submit-event' page creation. Will link to default TEC CE page. // 'submit-event' => [ // 'title' => 'Submit Event', @@ -253,7 +261,7 @@ function hvac_ce_enqueue_common_assets() { 'hvac-dashboard', 'community-login', 'trainer-registration', 'trainer-profile', 'manage-event', 'event-summary', 'email-attendees', 'certificate-reports', 'generate-certificates', 'certificate-fix', 'hvac-documentation', 'attendee-profile', - 'master-dashboard', 'google-sheets' + 'master-dashboard', 'google-sheets', 'communication-templates', 'communication-schedules' ]; // Only proceed if we're on an HVAC page diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-community-events.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-community-events.php index 26eef361..807672cb 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-community-events.php +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-hvac-community-events.php @@ -79,7 +79,12 @@ class HVAC_Community_Events { 'certificates/test-rewrite-rules.php', // Rewrite rules testing (temporary) 'google-sheets/class-google-sheets-auth.php', // Google Sheets authentication 'google-sheets/class-google-sheets-manager.php', // Google Sheets management - 'communication/class-communication-templates.php' // Email template management + 'communication/class-communication-templates.php', // Email template management + 'communication/class-communication-installer.php', // Communication system database installer + 'communication/class-communication-schedule-manager.php', // Communication schedule manager + 'communication/class-communication-trigger-engine.php', // Communication trigger engine + 'communication/class-communication-logger.php', // Communication logger + 'communication/class-communication-scheduler.php' // Communication scheduler ]; // Make sure Login_Handler is loaded first for shortcode registration $login_handler_path = HVAC_CE_PLUGIN_DIR . 'includes/community/class-login-handler.php'; @@ -302,6 +307,9 @@ class HVAC_Community_Events { if (class_exists('HVAC_Help_System')) { HVAC_Help_System::instance(); } + + // Initialize communication system + $this->init_communication_system(); } /** @@ -389,6 +397,9 @@ class HVAC_Community_Events { // Add communication templates shortcode add_shortcode('hvac_communication_templates', array($this, 'render_communication_templates')); + + // Add communication schedules shortcode + add_shortcode('hvac_communication_schedules', array($this, 'render_communication_schedules')); // Removed shortcode override - let The Events Calendar Community Events handle this shortcode // add_shortcode('tribe_community_events', array($this, 'render_tribe_community_events')); @@ -788,6 +799,25 @@ class HVAC_Community_Events { } } + /** + * Initialize communication system + */ + private function init_communication_system() { + // Check and install/update communication database tables + if (class_exists('HVAC_Communication_Installer')) { + HVAC_Communication_Installer::maybe_update(); + } + + // Initialize the communication scheduler (singleton) + if (class_exists('HVAC_Communication_Scheduler')) { + hvac_communication_scheduler(); + } + + if (class_exists('HVAC_Logger')) { + HVAC_Logger::info('Communication system initialized', 'Communication System'); + } + } + /** * Render communication templates content */ @@ -811,5 +841,29 @@ class HVAC_Community_Events { include HVAC_CE_PLUGIN_DIR . 'templates/communication/template-communication-templates.php'; return ob_get_clean(); } + + /** + * Render communication schedules content + */ + public function render_communication_schedules() { + if (!is_user_logged_in()) { + return '

Please log in to manage communication schedules.

'; + } + + // 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 '

You do not have permission to manage communication schedules.

'; + } + + // 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 \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-installer.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-installer.php new file mode 100644 index 00000000..46200d60 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-installer.php @@ -0,0 +1,383 @@ +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' ); + } + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-logger.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-logger.php new file mode 100644 index 00000000..39de5c12 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-logger.php @@ -0,0 +1,467 @@ +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; + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-schedule-manager.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-schedule-manager.php new file mode 100644 index 00000000..c5b54b91 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-schedule-manager.php @@ -0,0 +1,603 @@ +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; + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-scheduler.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-scheduler.php new file mode 100644 index 00000000..43f79ee5 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-scheduler.php @@ -0,0 +1,596 @@ +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' ); \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-trigger-engine.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-trigger-engine.php new file mode 100644 index 00000000..32054129 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/communication/class-communication-trigger-engine.php @@ -0,0 +1,519 @@ +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; + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/communication/template-communication-schedules.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/communication/template-communication-schedules.php new file mode 100644 index 00000000..d9fe054b --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/communication/template-communication-schedules.php @@ -0,0 +1,832 @@ +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; +?> + +
+ + +
+ +
+

Create New Schedule

+ +
+ + +
+
+ + +
+ +
+ + + Don't have templates? Create one here +
+
+ +
+
+ + +
+ +
+ + +
+
+ + + +
+ Trigger Settings + +
+
+ + +
+ +
+ +
+ + +
+
+
+ + +
+ +
+ Recurring Options (Optional) + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+

Your Schedules

+ + +
+

You haven't created any communication schedules yet.

+

Use the form above to create your first automated email schedule.

+
+ +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Schedule NameEventTemplateTriggerStatusNext RunRunsActions
+ + + + + + + All Events + + + + + + + + + + + + N/A + + + + + / + + + + + +
+
+ +
+ + +
+

Quick Start Templates

+

Use these pre-configured schedule templates to get started quickly.

+ +
+ get_default_schedule_templates(); + foreach ( $default_templates as $template_key => $template ) : + ?> +
+

+

+
+ + +
+ +
+ +
+
+
+ + + +
+ + + + \ No newline at end of file