diff --git a/wordpress-dev/tests/e2e/final-working-tests.test.ts b/wordpress-dev/tests/e2e/final-working-tests.test.ts index 1cfafc31..aef9d0dc 100644 --- a/wordpress-dev/tests/e2e/final-working-tests.test.ts +++ b/wordpress-dev/tests/e2e/final-working-tests.test.ts @@ -182,4 +182,282 @@ test.describe('HVAC Plugin - Final Working Tests', () => { expect(criticalErrors.length).toBe(0); console.log('✓ No critical errors found'); }); + + test('Attendee Profile - Icon visibility in Generate Certificates', async ({ authenticatedPage: page }) => { + test.setTimeout(25000); + const actions = new CommonActions(page); + + // Navigate to Generate Certificates + await actions.navigateAndWait('/generate-certificates/'); + console.log('✓ Generate Certificates page loaded'); + + // Select an event + const eventDropdown = page.locator('select[name="event_id"]'); + await expect(eventDropdown).toBeVisible(); + + // Get event options + const eventOptions = await eventDropdown.locator('option').count(); + if (eventOptions > 1) { + await eventDropdown.selectOption({ index: 1 }); + console.log('✓ Event selected'); + + // Wait for attendees to load + await actions.waitForComplexAjax(); + + // Check for attendee profile icons + const profileIcons = page.locator('.hvac-attendee-profile-icon'); + const iconCount = await profileIcons.count(); + + if (iconCount > 0) { + console.log(`✓ Found ${iconCount} profile icons`); + + // Verify icons have proper attributes + const firstIcon = profileIcons.first(); + await expect(firstIcon).toHaveAttribute('target', '_blank'); + await expect(firstIcon).toHaveAttribute('href', /attendee-profile.*attendee_id=/); + console.log('✓ Profile icons properly configured'); + } else { + console.log('⚠ No attendees found for testing profile icons'); + } + } else { + console.log('⚠ No events available for testing'); + } + + await actions.screenshot('attendee-profile-icons'); + }); + + test('Attendee Profile - Page accessibility and structure', async ({ authenticatedPage: page }) => { + test.setTimeout(25000); + const actions = new CommonActions(page); + + // First, get an attendee ID from Generate Certificates page + await actions.navigateAndWait('/generate-certificates/'); + + const eventDropdown = page.locator('select[name="event_id"]'); + if (await eventDropdown.isVisible()) { + await eventDropdown.selectOption({ index: 1 }); + await actions.waitForComplexAjax(); + + // Find first profile icon + const profileIcon = page.locator('.hvac-attendee-profile-icon').first(); + if (await profileIcon.count() > 0) { + // Get the href to extract attendee ID + const href = await profileIcon.getAttribute('href'); + console.log('✓ Found attendee profile link:', href); + + // Navigate to the profile page + await profileIcon.click(); + + // Switch to new tab + const newPage = await page.context().waitForEvent('page'); + await newPage.waitForLoadState('networkidle'); + + // Verify profile page structure + await expect(newPage).toHaveURL(/attendee-profile.*attendee_id=/); + console.log('✓ Attendee profile page opened'); + + // Check main sections + await expect(newPage.locator('.hvac-profile-header')).toBeVisible(); + console.log('✓ Profile header visible'); + + await expect(newPage.locator('.hvac-stats-section')).toBeVisible(); + console.log('✓ Statistics section visible'); + + await expect(newPage.locator('.hvac-profile-section')).toBeVisible(); + console.log('✓ Profile information section visible'); + + await expect(newPage.locator('.hvac-timeline-section')).toBeVisible(); + console.log('✓ Timeline section visible'); + + // Check statistics cards + const statCards = newPage.locator('.hvac-stat-card'); + await expect(statCards).toHaveCount(4); + console.log('✓ All 4 statistics cards present'); + + // Verify stat labels + await expect(newPage.locator('text=Purchases Made')).toBeVisible(); + await expect(newPage.locator('text=Events Registered')).toBeVisible(); + await expect(newPage.locator('text=Events Attended')).toBeVisible(); + await expect(newPage.locator('text=Certificates Earned')).toBeVisible(); + console.log('✓ All statistics labels correct'); + + // Check action buttons + await expect(newPage.locator('.hvac-profile-actions').locator('text=Email Attendee')).toBeVisible(); + await expect(newPage.locator('.hvac-profile-actions').locator('text=Print Profile')).toBeVisible(); + await expect(newPage.locator('.hvac-profile-actions').locator('text=Back')).toBeVisible(); + console.log('✓ Action buttons present'); + + await newPage.screenshot({ path: 'test-results/screenshots/attendee-profile-page.png' }); + await newPage.close(); + } else { + console.log('⚠ No attendees available for profile testing'); + } + } + }); + + test('Attendee Profile - Direct access with parameters', async ({ authenticatedPage: page }) => { + test.setTimeout(20000); + const actions = new CommonActions(page); + + // Test direct access without attendee_id + await actions.navigateAndWait('/attendee-profile/'); + await expect(page.locator('text=No attendee specified')).toBeVisible(); + console.log('✓ Handles missing attendee_id correctly'); + + // Test with invalid attendee_id + await actions.navigateAndWait('/attendee-profile/?attendee_id=999999'); + await expect(page.locator('text=Attendee not found')).toBeVisible(); + console.log('✓ Handles invalid attendee_id correctly'); + + await actions.screenshot('attendee-profile-errors'); + }); + + test('Attendee Profile - Chart rendering', async ({ authenticatedPage: page }) => { + test.setTimeout(30000); + const actions = new CommonActions(page); + + // Navigate to Generate Certificates to find an attendee + await actions.navigateAndWait('/generate-certificates/'); + + const eventDropdown = page.locator('select[name="event_id"]'); + if (await eventDropdown.isVisible()) { + await eventDropdown.selectOption({ index: 1 }); + await actions.waitForComplexAjax(); + + const profileIcon = page.locator('.hvac-attendee-profile-icon').first(); + if (await profileIcon.count() > 0) { + await profileIcon.click(); + + // Switch to profile tab + const profilePage = await page.context().waitForEvent('page'); + await profilePage.waitForLoadState('networkidle'); + + // Check for chart container + const chartSection = profilePage.locator('.hvac-timeline-chart-section'); + await expect(chartSection).toBeVisible(); + console.log('✓ Chart section visible'); + + // Check for canvas element + const canvas = profilePage.locator('#hvac-timeline-chart'); + await expect(canvas).toBeVisible(); + console.log('✓ Chart canvas present'); + + // Wait for chart to potentially render + await profilePage.waitForTimeout(2000); + + // Check if Chart.js is loaded + const hasChart = await profilePage.evaluate(() => { + return typeof window.Chart !== 'undefined'; + }); + expect(hasChart).toBeTruthy(); + console.log('✓ Chart.js library loaded'); + + await profilePage.close(); + } + } + }); + + test('Attendee Profile - Timeline interactions', async ({ authenticatedPage: page }) => { + test.setTimeout(25000); + const actions = new CommonActions(page); + + // Navigate to an attendee profile with activity + await actions.navigateAndWait('/generate-certificates/'); + + const eventDropdown = page.locator('select[name="event_id"]'); + if (await eventDropdown.isVisible()) { + // Select an event that likely has checked-in attendees + await eventDropdown.selectOption({ index: 1 }); + await actions.waitForComplexAjax(); + + // Look for a checked-in attendee + const checkedInRow = page.locator('tr:has-text("Checked In")').first(); + if (await checkedInRow.count() > 0) { + const profileIcon = checkedInRow.locator('.hvac-attendee-profile-icon'); + await profileIcon.click(); + + const profilePage = await page.context().waitForEvent('page'); + await profilePage.waitForLoadState('networkidle'); + + // Check timeline items + const timelineItems = profilePage.locator('.hvac-timeline-item'); + const itemCount = await timelineItems.count(); + + if (itemCount > 0) { + console.log(`✓ Found ${itemCount} timeline items`); + + // Check timeline item structure + const firstItem = timelineItems.first(); + await expect(firstItem.locator('.hvac-timeline-date')).toBeVisible(); + await expect(firstItem.locator('.hvac-timeline-marker')).toBeVisible(); + await expect(firstItem.locator('.hvac-timeline-content')).toBeVisible(); + console.log('✓ Timeline item structure correct'); + + // Check for different event types + const registrationItem = profilePage.locator('.hvac-timeline-item[data-type="registration"]'); + if (await registrationItem.count() > 0) { + console.log('✓ Registration events present'); + } + + const eventItem = profilePage.locator('.hvac-timeline-item[data-type="event"]'); + if (await eventItem.count() > 0) { + console.log('✓ Event attendance items present'); + } + + const certificateItem = profilePage.locator('.hvac-timeline-item[data-type="certificate"]'); + if (await certificateItem.count() > 0) { + console.log('✓ Certificate items present'); + } + } else { + console.log('⚠ No timeline items found'); + } + + await profilePage.close(); + } else { + console.log('⚠ No checked-in attendees found for timeline testing'); + } + } + }); + + test('Attendee Profile - Print functionality', async ({ authenticatedPage: page }) => { + test.setTimeout(20000); + const actions = new CommonActions(page); + + // Navigate to any attendee profile + await actions.navigateAndWait('/generate-certificates/'); + const eventDropdown = page.locator('select[name="event_id"]'); + + if (await eventDropdown.isVisible()) { + await eventDropdown.selectOption({ index: 1 }); + await actions.waitForComplexAjax(); + + const profileIcon = page.locator('.hvac-attendee-profile-icon').first(); + if (await profileIcon.count() > 0) { + await profileIcon.click(); + + const profilePage = await page.context().waitForEvent('page'); + await profilePage.waitForLoadState('networkidle'); + + // Check print button + const printButton = profilePage.locator('button:has-text("Print Profile")'); + await expect(printButton).toBeVisible(); + + // Verify print CSS is loaded + const hasPrintStyles = await profilePage.evaluate(() => { + const styleSheets = Array.from(document.styleSheets); + return styleSheets.some(sheet => { + try { + return sheet.href && sheet.href.includes('hvac-attendee-profile.css'); + } catch (e) { + return false; + } + }); + }); + expect(hasPrintStyles).toBeTruthy(); + console.log('✓ Print styles loaded'); + + await profilePage.close(); + } + } + }); }); \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-attendee-profile.css b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-attendee-profile.css new file mode 100644 index 00000000..ab2dd614 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/css/hvac-attendee-profile.css @@ -0,0 +1,524 @@ +/** + * HVAC Attendee Profile Styles + */ + +.hvac-attendee-profile { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +/* Profile Header */ +.hvac-profile-header { + display: flex; + align-items: center; + gap: 30px; + background: #fff; + padding: 30px; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + margin-bottom: 30px; +} + +.hvac-profile-avatar { + flex-shrink: 0; +} + +.hvac-profile-avatar img { + width: 120px; + height: 120px; + border-radius: 50%; + object-fit: cover; + border: 4px solid #f0f4f8; +} + +.hvac-avatar-placeholder { + width: 120px; + height: 120px; + border-radius: 50%; + background: #f0f4f8; + display: flex; + align-items: center; + justify-content: center; + color: #8b95a1; +} + +.hvac-avatar-placeholder i { + font-size: 80px; +} + +.hvac-profile-title h1 { + margin: 0 0 10px 0; + font-size: 32px; + color: #1e293b; + font-weight: 600; +} + +.hvac-profile-email { + margin: 0; + color: #64748b; + font-size: 16px; +} + +/* Statistics Section */ +.hvac-stats-section { + margin-bottom: 40px; +} + +.hvac-stats-section h2 { + font-size: 24px; + color: #1e293b; + margin-bottom: 20px; + display: flex; + align-items: center; + gap: 10px; +} + +.hvac-stats-section h2 i { + color: #007cba; + font-size: 20px; +} + +.hvac-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; +} + +.hvac-stat-card { + background: #fff; + border-radius: 12px; + padding: 25px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + display: flex; + align-items: center; + gap: 20px; + transition: transform 0.2s ease; +} + +.hvac-stat-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); +} + +.hvac-stat-icon { + width: 60px; + height: 60px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + flex-shrink: 0; +} + +.hvac-stat-card:nth-child(1) .hvac-stat-icon { + background: #e0f2fe; + color: #0284c7; +} + +.hvac-stat-card:nth-child(2) .hvac-stat-icon { + background: #f0fdf4; + color: #16a34a; +} + +.hvac-stat-card:nth-child(3) .hvac-stat-icon { + background: #fef3c7; + color: #d97706; +} + +.hvac-stat-card:nth-child(4) .hvac-stat-icon { + background: #fce7f3; + color: #db2777; +} + +.hvac-stat-content { + flex: 1; +} + +.hvac-stat-value { + font-size: 32px; + font-weight: 700; + color: #1e293b; + line-height: 1; + margin-bottom: 5px; +} + +.hvac-stat-label { + color: #64748b; + font-size: 14px; + font-weight: 500; +} + +/* Profile Information Section */ +.hvac-profile-section { + background: #fff; + border-radius: 12px; + padding: 30px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + margin-bottom: 40px; +} + +.hvac-profile-section h2 { + font-size: 24px; + color: #1e293b; + margin-bottom: 25px; + display: flex; + align-items: center; + gap: 10px; +} + +.hvac-profile-section h2 i { + color: #007cba; + font-size: 20px; +} + +.hvac-profile-info { + display: flex; + flex-direction: column; + gap: 20px; +} + +.hvac-info-row { + display: flex; + align-items: center; + padding: 15px 0; + border-bottom: 1px solid #f1f5f9; +} + +.hvac-info-row:last-child { + border-bottom: none; +} + +.hvac-info-label { + flex: 0 0 200px; + color: #64748b; + font-weight: 500; + display: flex; + align-items: center; + gap: 10px; +} + +.hvac-info-label i { + width: 20px; + text-align: center; + color: #94a3b8; +} + +.hvac-info-value { + flex: 1; + color: #1e293b; + font-weight: 500; +} + +.hvac-info-value a { + color: #007cba; + text-decoration: none; +} + +.hvac-info-value a:hover { + text-decoration: underline; +} + +/* Timeline Section */ +.hvac-timeline-section { + margin-bottom: 40px; +} + +.hvac-timeline-section h2 { + font-size: 24px; + color: #1e293b; + margin-bottom: 25px; + display: flex; + align-items: center; + gap: 10px; +} + +.hvac-timeline-section h2 i { + color: #007cba; + font-size: 20px; +} + +.hvac-no-activity { + text-align: center; + color: #64748b; + padding: 40px; + background: #f8fafc; + border-radius: 8px; +} + +.hvac-timeline { + position: relative; + padding-left: 40px; +} + +.hvac-timeline-item { + position: relative; + margin-bottom: 40px; + display: flex; + align-items: flex-start; + gap: 30px; +} + +.hvac-timeline-date { + flex: 0 0 150px; + text-align: right; + padding-top: 5px; +} + +.hvac-timeline-date { + font-weight: 600; + color: #1e293b; + font-size: 14px; +} + +.hvac-timeline-time { + display: block; + font-weight: 400; + color: #94a3b8; + font-size: 12px; + margin-top: 2px; +} + +.hvac-timeline-marker { + position: absolute; + left: 168px; + top: 0; + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + z-index: 2; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.hvac-timeline-marker i { + font-size: 16px; +} + +.hvac-timeline-connector { + position: absolute; + left: 187px; + top: 40px; + width: 2px; + height: calc(100% + 20px); + background: #e2e8f0; + z-index: 1; +} + +.hvac-timeline-content { + flex: 1; + background: #fff; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + margin-left: 40px; +} + +.hvac-timeline-content h4 { + margin: 0 0 10px 0; + color: #1e293b; + font-size: 16px; + font-weight: 600; +} + +.hvac-checkin-status { + display: inline-block; + padding: 4px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 500; + margin-right: 10px; +} + +.hvac-checkin-status.checked-in { + background: #dcfce7; + color: #15803d; +} + +.hvac-checkin-status.not-checked-in { + background: #f3f4f6; + color: #6b7280; +} + +.hvac-certificate-number { + display: inline-block; + padding: 4px 12px; + background: #fef3c7; + color: #a16207; + border-radius: 20px; + font-size: 12px; + font-weight: 500; + margin-right: 10px; +} + +.hvac-event-link { + display: inline-block; + margin-top: 10px; + color: #007cba; + text-decoration: none; + font-size: 14px; + font-weight: 500; +} + +.hvac-event-link:hover { + text-decoration: underline; +} + +.hvac-event-link i { + font-size: 12px; + margin-left: 5px; +} + +/* Timeline Chart Section */ +.hvac-timeline-chart-section { + background: #fff; + border-radius: 12px; + padding: 30px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + margin-bottom: 40px; +} + +.hvac-timeline-chart-section h2 { + font-size: 24px; + color: #1e293b; + margin-bottom: 25px; + display: flex; + align-items: center; + gap: 10px; +} + +.hvac-timeline-chart-section h2 i { + color: #007cba; + font-size: 20px; +} + +.hvac-chart-container { + position: relative; + height: 400px; +} + +/* Action Buttons */ +.hvac-profile-actions { + display: flex; + gap: 15px; + justify-content: center; + margin-top: 40px; +} + +.hvac-button { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 12px 24px; + border-radius: 8px; + font-weight: 500; + text-decoration: none; + transition: all 0.2s ease; + border: none; + cursor: pointer; + font-size: 16px; +} + +.hvac-button.hvac-primary { + background: #007cba; + color: #fff; +} + +.hvac-button.hvac-primary:hover { + background: #005a87; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 124, 186, 0.3); +} + +.hvac-button.hvac-secondary { + background: #f3f4f6; + color: #374151; +} + +.hvac-button.hvac-secondary:hover { + background: #e5e7eb; +} + +.hvac-button.hvac-ghost { + background: transparent; + color: #6b7280; + border: 1px solid #e5e7eb; +} + +.hvac-button.hvac-ghost:hover { + background: #f9fafb; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .hvac-profile-header { + flex-direction: column; + text-align: center; + } + + .hvac-stats-grid { + grid-template-columns: 1fr; + } + + .hvac-info-row { + flex-direction: column; + align-items: flex-start; + gap: 5px; + } + + .hvac-info-label { + flex: unset; + } + + .hvac-timeline-item { + flex-direction: column; + padding-left: 50px; + } + + .hvac-timeline-date { + text-align: left; + flex: unset; + order: 2; + margin-top: 10px; + } + + .hvac-timeline-marker { + left: 0; + } + + .hvac-timeline-connector { + left: 19px; + } + + .hvac-timeline-content { + margin-left: 0; + } + + .hvac-profile-actions { + flex-direction: column; + } + + .hvac-button { + width: 100%; + justify-content: center; + } +} + +/* Print Styles */ +@media print { + .hvac-profile-actions { + display: none; + } + + .hvac-timeline-chart-section { + page-break-inside: avoid; + } + + .hvac-timeline-item { + page-break-inside: avoid; + } +} \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/hvac-attendee-profile.js b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/hvac-attendee-profile.js new file mode 100644 index 00000000..3d2f20aa --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/assets/js/hvac-attendee-profile.js @@ -0,0 +1,288 @@ +/** + * HVAC Attendee Profile JavaScript + */ + +(function($) { + 'use strict'; + + $(document).ready(function() { + // Initialize timeline chart if data exists + if (typeof hvacTimelineData !== 'undefined' && hvacTimelineData.length > 0) { + initializeTimelineChart(); + } + + // Add smooth scrolling for timeline + smoothScrollTimeline(); + + // Initialize tooltips + initializeTooltips(); + }); + + /** + * Initialize the timeline chart using Chart.js + */ + function initializeTimelineChart() { + const ctx = document.getElementById('hvac-timeline-chart'); + if (!ctx) return; + + // Process timeline data for the chart + const chartData = processTimelineData(); + + // Create the chart + new Chart(ctx, { + type: 'line', + data: { + labels: chartData.labels, + datasets: [ + { + label: 'Registrations', + data: chartData.registrations, + borderColor: '#007cba', + backgroundColor: 'rgba(0, 124, 186, 0.1)', + borderWidth: 2, + pointRadius: 4, + pointHoverRadius: 6, + tension: 0.4 + }, + { + label: 'Events Attended', + data: chartData.attended, + borderColor: '#28a745', + backgroundColor: 'rgba(40, 167, 69, 0.1)', + borderWidth: 2, + pointRadius: 4, + pointHoverRadius: 6, + tension: 0.4 + }, + { + label: 'Certificates Earned', + data: chartData.certificates, + borderColor: '#ffc107', + backgroundColor: 'rgba(255, 193, 7, 0.1)', + borderWidth: 2, + pointRadius: 4, + pointHoverRadius: 6, + tension: 0.4 + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: 'index', + intersect: false + }, + plugins: { + legend: { + position: 'top', + labels: { + usePointStyle: true, + padding: 15, + font: { + size: 14 + } + } + }, + tooltip: { + backgroundColor: 'rgba(0, 0, 0, 0.8)', + titleFont: { + size: 14 + }, + bodyFont: { + size: 13 + }, + padding: 12, + cornerRadius: 8, + displayColors: true, + callbacks: { + title: function(tooltipItems) { + return tooltipItems[0].label; + } + } + } + }, + scales: { + x: { + display: true, + title: { + display: true, + text: 'Month', + font: { + size: 14 + } + }, + grid: { + display: false + } + }, + y: { + display: true, + title: { + display: true, + text: 'Count', + font: { + size: 14 + } + }, + beginAtZero: true, + ticks: { + stepSize: 1, + precision: 0 + }, + grid: { + borderDash: [5, 5] + } + } + } + } + }); + } + + /** + * Process timeline data for chart display + */ + function processTimelineData() { + // Group events by month + const monthlyData = {}; + + hvacTimelineData.forEach(function(event) { + const date = new Date(event.date); + const monthKey = date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0'); + + if (!monthlyData[monthKey]) { + monthlyData[monthKey] = { + registrations: 0, + attended: 0, + certificates: 0 + }; + } + + switch(event.type) { + case 'registration': + monthlyData[monthKey].registrations++; + break; + case 'event': + if (event.checked_in) { + monthlyData[monthKey].attended++; + } + break; + case 'certificate': + monthlyData[monthKey].certificates++; + break; + } + }); + + // Sort months and create arrays for chart + const sortedMonths = Object.keys(monthlyData).sort(); + const labels = []; + const registrations = []; + const attended = []; + const certificates = []; + + // Get last 12 months of data + const lastMonths = sortedMonths.slice(-12); + + lastMonths.forEach(function(monthKey) { + const [year, month] = monthKey.split('-'); + const date = new Date(year, month - 1); + labels.push(date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })); + + registrations.push(monthlyData[monthKey].registrations); + attended.push(monthlyData[monthKey].attended); + certificates.push(monthlyData[monthKey].certificates); + }); + + return { + labels: labels, + registrations: registrations, + attended: attended, + certificates: certificates + }; + } + + /** + * Add smooth scrolling to timeline + */ + function smoothScrollTimeline() { + // Add hover effect to timeline items + $('.hvac-timeline-item').on('mouseenter', function() { + $(this).find('.hvac-timeline-content').addClass('hover'); + }).on('mouseleave', function() { + $(this).find('.hvac-timeline-content').removeClass('hover'); + }); + + // Add animation on scroll + const observer = new IntersectionObserver(function(entries) { + entries.forEach(function(entry) { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + } + }); + }, { + threshold: 0.1 + }); + + document.querySelectorAll('.hvac-timeline-item').forEach(function(item) { + observer.observe(item); + }); + } + + /** + * Initialize tooltips + */ + function initializeTooltips() { + // Add tooltips to stat cards + $('.hvac-stat-card').each(function() { + const label = $(this).find('.hvac-stat-label').text(); + $(this).attr('title', 'View ' + label + ' details'); + }); + + // Add tooltips to timeline markers + $('.hvac-timeline-marker').each(function() { + const item = $(this).closest('.hvac-timeline-item'); + const type = item.data('type'); + let tooltip = ''; + + switch(type) { + case 'registration': + tooltip = 'Event Registration'; + break; + case 'event': + tooltip = 'Event Attendance'; + break; + case 'certificate': + tooltip = 'Certificate Earned'; + break; + } + + $(this).attr('title', tooltip); + }); + } + + // Add CSS for animations + const style = document.createElement('style'); + style.innerHTML = ` + .hvac-timeline-item { + opacity: 0; + transform: translateY(20px); + transition: opacity 0.5s ease, transform 0.5s ease; + } + + .hvac-timeline-item.visible { + opacity: 1; + transform: translateY(0); + } + + .hvac-timeline-content { + transition: box-shadow 0.3s ease, transform 0.3s ease; + } + + .hvac-timeline-content.hover { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); + transform: translateX(5px); + } + `; + document.head.appendChild(style); + +})(jQuery); \ 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 6db32114..3646805f 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 @@ -87,6 +87,10 @@ function hvac_ce_create_required_pages() { 'title' => 'Trainer Documentation', 'content' => '[hvac_documentation]', ], + 'attendee-profile' => [ // Add attendee profile page + 'title' => 'Attendee Profile', + 'content' => '[hvac_attendee_profile]', + ], // REMOVED: 'submit-event' page creation. Will link to default TEC CE page. // 'submit-event' => [ // 'title' => 'Submit Event', @@ -229,7 +233,7 @@ function hvac_ce_enqueue_common_assets() { $hvac_pages = [ 'hvac-dashboard', 'community-login', 'trainer-registration', 'trainer-profile', 'manage-event', 'event-summary', 'email-attendees', 'certificate-reports', - 'generate-certificates', 'certificate-fix', 'hvac-documentation' + 'generate-certificates', 'certificate-fix', 'hvac-documentation', 'attendee-profile' ]; // Only proceed if we're on an HVAC page @@ -520,3 +524,26 @@ function hvac_ce_include_order_summary_template( $template ) { } // Removed - template handling is now in the main class // add_filter( 'template_include', 'hvac_ce_include_event_summary_template', 99 ); + +/** + * Initialize attendee profile handler + */ +function hvac_init_attendee_profile() { + // Load the attendee profile class if not already loaded + if (!class_exists('HVAC_Attendee_Profile')) { + $profile_file = HVAC_CE_PLUGIN_DIR . 'includes/class-attendee-profile.php'; + if (file_exists($profile_file)) { + require_once $profile_file; + } + } + + // Initialize the handler if class exists + if (class_exists('HVAC_Attendee_Profile')) { + HVAC_Attendee_Profile::instance(); + } +} +// Initialize on plugins_loaded +add_action('plugins_loaded', 'hvac_init_attendee_profile', 10); + +// Include attendee profile helper functions +require_once HVAC_CE_PLUGIN_DIR . 'includes/helpers/attendee-profile-link.php'; diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-attendee-profile.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-attendee-profile.php new file mode 100644 index 00000000..7fc82dd1 --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/class-attendee-profile.php @@ -0,0 +1,422 @@ + admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('hvac_attendee_nonce') + )); + } + + /** + * Render attendee profile + */ + public function render_attendee_profile($atts) { + // Check permissions - trainers and admins only + if (!current_user_can('manage_hvac_events') && !current_user_can('manage_options')) { + return '

You do not have permission to view attendee profiles.

'; + } + + // Get attendee ID from URL parameter + $attendee_id = isset($_GET['attendee_id']) ? intval($_GET['attendee_id']) : 0; + + if (!$attendee_id) { + return '

No attendee specified. Please provide an attendee ID.

'; + } + + // Get attendee data + $attendee_data = $this->get_attendee_data($attendee_id); + + if (!$attendee_data) { + return '

Attendee not found.

'; + } + + ob_start(); + include HVAC_CE_PLUGIN_DIR . 'templates/attendee/template-attendee-profile.php'; + return ob_get_clean(); + } + + /** + * Get comprehensive attendee data + */ + public function get_attendee_data($attendee_id) { + global $wpdb; + + // First, try to get attendee as a user + $user = null; + $attendee_email = ''; + + // Check if this is an attendee post ID + $attendee_post = get_post($attendee_id); + if ($attendee_post && in_array($attendee_post->post_type, array('tribe_tpp_attendees', 'tec_tc_attendee', 'tribe_rsvp_attendees'))) { + // Get email from attendee meta + $email_keys = array( + '_tec_tickets_commerce_email', + '_tribe_tpp_email', + '_tribe_tickets_email', + '_tribe_tpp_attendee_email' + ); + + foreach ($email_keys as $key) { + $email = get_post_meta($attendee_id, $key, true); + if ($email && is_email($email)) { + $attendee_email = $email; + break; + } + } + + // Try to find user by email + if ($attendee_email) { + $user = get_user_by('email', $attendee_email); + } + } else { + // Maybe this is a user ID + $user = get_user_by('id', $attendee_id); + if ($user) { + $attendee_email = $user->user_email; + } + } + + if (!$attendee_email) { + return false; + } + + // Get profile information + $profile_data = $this->get_profile_info($attendee_email, $user); + + // Get statistics + $stats = $this->get_attendee_statistics($attendee_email); + + // Get timeline data + $timeline = $this->get_attendee_timeline($attendee_email); + + return array( + 'profile' => $profile_data, + 'stats' => $stats, + 'timeline' => $timeline, + 'attendee_id' => $attendee_id, + 'user_id' => $user ? $user->ID : 0 + ); + } + + /** + * Get profile information + */ + private function get_profile_info($email, $user = null) { + global $wpdb; + + $profile = array( + 'name' => '', + 'email' => $email, + 'phone' => '', + 'company' => '', + 'state' => '', + 'avatar_url' => '' + ); + + // If we have a user, get their data + if ($user) { + $profile['name'] = $user->display_name; + $profile['phone'] = get_user_meta($user->ID, 'phone', true); + $profile['company'] = get_user_meta($user->ID, 'company', true); + $profile['state'] = get_user_meta($user->ID, 'state', true); + $profile['avatar_url'] = get_avatar_url($user->ID, array('size' => 150)); + } + + // Try to get name from attendee records if not set + if (empty($profile['name'])) { + $name = $wpdb->get_var($wpdb->prepare(" + SELECT COALESCE( + MAX(CASE WHEN pm.meta_key = '_tec_tickets_commerce_full_name' THEN pm.meta_value END), + MAX(CASE WHEN pm.meta_key = '_tribe_tpp_full_name' THEN pm.meta_value END), + MAX(CASE WHEN pm.meta_key = '_tribe_tickets_full_name' THEN pm.meta_value END) + ) + FROM {$wpdb->posts} p + JOIN {$wpdb->postmeta} pm_email ON p.ID = pm_email.post_id + JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id + WHERE p.post_type IN ('tribe_tpp_attendees', 'tec_tc_attendee', 'tribe_rsvp_attendees') + AND pm_email.meta_value = %s + AND pm_email.meta_key IN ('_tec_tickets_commerce_email', '_tribe_tpp_email', '_tribe_tickets_email') + AND pm.meta_key IN ('_tec_tickets_commerce_full_name', '_tribe_tpp_full_name', '_tribe_tickets_full_name') + LIMIT 1 + ", $email)); + + if ($name) { + $profile['name'] = $name; + } + } + + // If still no name, use email prefix + if (empty($profile['name'])) { + $email_parts = explode('@', $email); + $profile['name'] = ucwords(str_replace(array('.', '_', '-'), ' ', $email_parts[0])); + } + + return $profile; + } + + /** + * Get attendee statistics + */ + private function get_attendee_statistics($email) { + global $wpdb; + + // Get total purchases (unique events registered) + $total_purchases = $wpdb->get_var($wpdb->prepare(" + SELECT COUNT(DISTINCT p.post_parent) + FROM {$wpdb->posts} p + JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id + WHERE p.post_type IN ('tribe_tpp_attendees', 'tec_tc_attendee', 'tribe_rsvp_attendees') + AND pm.meta_key IN ('_tec_tickets_commerce_email', '_tribe_tpp_email', '_tribe_tickets_email', '_tribe_tpp_attendee_email') + AND pm.meta_value = %s + AND p.post_status = 'publish' + ", $email)); + + // Get events registered for + $events_registered = $total_purchases; // Same as purchases for now + + // Get events checked in + $events_checked_in = $wpdb->get_var($wpdb->prepare(" + SELECT COUNT(DISTINCT p.post_parent) + FROM {$wpdb->posts} p + JOIN {$wpdb->postmeta} pm_email ON p.ID = pm_email.post_id + JOIN {$wpdb->postmeta} pm_checkin ON p.ID = pm_checkin.post_id + WHERE p.post_type IN ('tribe_tpp_attendees', 'tec_tc_attendee', 'tribe_rsvp_attendees') + AND pm_email.meta_key IN ('_tec_tickets_commerce_email', '_tribe_tpp_email', '_tribe_tickets_email', '_tribe_tpp_attendee_email') + AND pm_email.meta_value = %s + AND pm_checkin.meta_key = '_tribe_tickets_attendee_checked_in' + AND pm_checkin.meta_value = '1' + AND p.post_status = 'publish' + ", $email)); + + // Get certificates earned + $certificates_earned = 0; + if (class_exists('HVAC_Certificate_Manager')) { + $certificate_manager = HVAC_Certificate_Manager::instance(); + + // Get all attendee IDs for this email + $attendee_ids = $wpdb->get_col($wpdb->prepare(" + SELECT p.ID + FROM {$wpdb->posts} p + JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id + WHERE p.post_type IN ('tribe_tpp_attendees', 'tec_tc_attendee', 'tribe_rsvp_attendees') + AND pm.meta_key IN ('_tec_tickets_commerce_email', '_tribe_tpp_email', '_tribe_tickets_email', '_tribe_tpp_attendee_email') + AND pm.meta_value = %s + AND p.post_status = 'publish' + ", $email)); + + // Count certificates for these attendees + foreach ($attendee_ids as $att_id) { + $certs = $wpdb->get_var($wpdb->prepare(" + SELECT COUNT(*) FROM {$wpdb->prefix}hvac_certificates + WHERE attendee_id = %d + ", $att_id)); + $certificates_earned += $certs; + } + } + + return array( + 'total_purchases' => intval($total_purchases), + 'events_registered' => intval($events_registered), + 'events_checked_in' => intval($events_checked_in), + 'certificates_earned' => intval($certificates_earned) + ); + } + + /** + * Get attendee timeline data + */ + private function get_attendee_timeline($email) { + global $wpdb; + + $timeline_events = array(); + + // Get all attendee records for this email + $attendee_records = $wpdb->get_results($wpdb->prepare(" + SELECT + p.ID as attendee_id, + p.post_parent as event_id, + p.post_date as registration_date, + event.post_title as event_title, + event_date.meta_value as event_date, + checkin.meta_value as checked_in + FROM {$wpdb->posts} p + JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id + JOIN {$wpdb->posts} event ON p.post_parent = event.ID + LEFT JOIN {$wpdb->postmeta} event_date ON event.ID = event_date.post_id AND event_date.meta_key = '_EventStartDate' + LEFT JOIN {$wpdb->postmeta} checkin ON p.ID = checkin.post_id AND checkin.meta_key = '_tribe_tickets_attendee_checked_in' + WHERE p.post_type IN ('tribe_tpp_attendees', 'tec_tc_attendee', 'tribe_rsvp_attendees') + AND pm.meta_key IN ('_tec_tickets_commerce_email', '_tribe_tpp_email', '_tribe_tickets_email', '_tribe_tpp_attendee_email') + AND pm.meta_value = %s + AND p.post_status = 'publish' + ORDER BY p.post_date DESC + ", $email)); + + foreach ($attendee_records as $record) { + // Registration event + $timeline_events[] = array( + 'type' => 'registration', + 'title' => 'Registered for ' . $record->event_title, + 'date' => $record->registration_date, + 'event_id' => $record->event_id, + 'icon' => 'fas fa-ticket-alt', + 'color' => '#007cba' + ); + + // Event attendance (if event date has passed) + if ($record->event_date && strtotime($record->event_date) < current_time('timestamp')) { + $timeline_events[] = array( + 'type' => 'event', + 'title' => 'Attended ' . $record->event_title, + 'date' => $record->event_date, + 'event_id' => $record->event_id, + 'checked_in' => $record->checked_in == '1', + 'icon' => 'fas fa-calendar-check', + 'color' => $record->checked_in == '1' ? '#28a745' : '#6c757d' + ); + } + + // Certificate earned + if (class_exists('HVAC_Certificate_Manager')) { + $cert = $wpdb->get_row($wpdb->prepare(" + SELECT certificate_number, date_generated + FROM {$wpdb->prefix}hvac_certificates + WHERE attendee_id = %d + LIMIT 1 + ", $record->attendee_id)); + + if ($cert) { + $timeline_events[] = array( + 'type' => 'certificate', + 'title' => 'Certificate earned for ' . $record->event_title, + 'date' => $cert->date_generated, + 'event_id' => $record->event_id, + 'certificate_number' => $cert->certificate_number, + 'icon' => 'fas fa-certificate', + 'color' => '#ffc107' + ); + } + } + } + + // Sort timeline by date + usort($timeline_events, function($a, $b) { + return strtotime($b['date']) - strtotime($a['date']); + }); + + return $timeline_events; + } + + /** + * AJAX handler for timeline data + */ + public function ajax_get_timeline() { + check_ajax_referer('hvac_attendee_nonce', 'nonce'); + + if (!current_user_can('manage_hvac_events') && !current_user_can('manage_options')) { + wp_die('Unauthorized'); + } + + $attendee_id = isset($_POST['attendee_id']) ? intval($_POST['attendee_id']) : 0; + + if (!$attendee_id) { + wp_send_json_error('Invalid attendee ID'); + } + + $attendee_data = $this->get_attendee_data($attendee_id); + + if (!$attendee_data) { + wp_send_json_error('Attendee not found'); + } + + wp_send_json_success(array( + 'timeline' => $attendee_data['timeline'] + )); + } +} + +// Initialize +HVAC_Attendee_Profile::instance(); \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/helpers/attendee-profile-link.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/helpers/attendee-profile-link.php new file mode 100644 index 00000000..2f5410ae --- /dev/null +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/includes/helpers/attendee-profile-link.php @@ -0,0 +1,150 @@ +attendee_id) ? $attendee->attendee_id : + (isset($attendee->ID) ? $attendee->ID : 0); + } else { + $attendee_id = intval($attendee); + } + + if (!$attendee_id) { + return ''; + } + + // Get profile page URL + $profile_page = get_page_by_path('attendee-profile'); + if (!$profile_page) { + return ''; + } + + $profile_url = add_query_arg('attendee_id', $attendee_id, get_permalink($profile_page->ID)); + + // Default link text + if (empty($link_text)) { + $link_text = __('View Profile', 'hvac-community-events'); + } + + // Build classes + $class_list = array('hvac-attendee-profile-link'); + if (!empty($classes)) { + $class_list = array_merge($class_list, (array)$classes); + } + + // Generate link + return sprintf( + '%s ', + esc_url($profile_url), + esc_attr(implode(' ', $class_list)), + esc_attr__('View attendee profile', 'hvac-community-events'), + esc_html($link_text) + ); +} + +/** + * Generate a small icon link to view an attendee's profile + * + * @param int|object $attendee Attendee ID or attendee object + * @return string HTML icon link to attendee profile + */ +function hvac_get_attendee_profile_icon($attendee) { + // Get attendee ID + $attendee_id = 0; + if (is_object($attendee)) { + $attendee_id = isset($attendee->attendee_id) ? $attendee->attendee_id : + (isset($attendee->ID) ? $attendee->ID : 0); + } else { + $attendee_id = intval($attendee); + } + + if (!$attendee_id) { + return ''; + } + + // Get profile page URL + $profile_page = get_page_by_path('attendee-profile'); + if (!$profile_page) { + return ''; + } + + $profile_url = add_query_arg('attendee_id', $attendee_id, get_permalink($profile_page->ID)); + + // Generate icon link + return sprintf( + '', + esc_url($profile_url), + esc_attr__('View attendee profile', 'hvac-community-events') + ); +} + +/** + * Add profile link styles to pages that show attendee lists + */ +function hvac_attendee_profile_link_styles() { + ?> + + + +
+ + +
+
+ + <?php echo esc_attr($profile['name']); ?> + +
+ +
+ +
+
+

+

+
+
+ + +
+

+ + Activity Summary +

+
+
+
+ +
+
+
+
Purchases Made
+
+
+ +
+
+ +
+
+
+
Events Registered
+
+
+ +
+
+ +
+
+
+
Events Attended
+
+
+ +
+
+ +
+
+
+
Certificates Earned
+
+
+
+
+ + +
+

+ + Profile Information +

+
+
+
+ + Name +
+
+
+ +
+
+ + Email +
+
+ +
+
+ + +
+
+ + Phone +
+
+ +
+
+ + + +
+
+ + Company +
+
+
+ + + +
+
+ + State +
+
+
+ +
+
+ + +
+

+ + Activity Timeline +

+ + +

No activity recorded for this attendee.

+ +
+ $event): ?> +
+
+ + +
+
+ +
+
+

+ + + + + + + + Certificate # + + + + + View Event + + +
+ +
+ +
+ +
+ +
+ + +
+

+ + Activity Over Time +

+
+ +
+
+ + +
+ + Email Attendee + + + + Back + +
+
+ + \ No newline at end of file diff --git a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/certificates/template-generate-certificates.php b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/certificates/template-generate-certificates.php index 08a462c0..3eea0f30 100644 --- a/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/certificates/template-generate-certificates.php +++ b/wordpress-dev/wordpress/wp-content/plugins/hvac-community-events/templates/certificates/template-generate-certificates.php @@ -213,7 +213,10 @@ get_header(); - + + + +