feat: Add comprehensive attendee profile feature with tests

## Summary
• Implemented attendee profile page showing stats, profile info, and activity timeline
• Added profile icon links in Generate Certificates page
• Created comprehensive E2E test suite covering all functionality

## Features Added
- Complete attendee profile page at /attendee-profile/
- Profile statistics: purchases, events registered/attended, certificates
- Personal information display with privacy-conscious handling
- Activity timeline with registration, event, and certificate history
- Monthly activity chart using Chart.js
- Print-friendly styling and layout
- Profile icon integration in certificate generation page

## Technical Details
- New PHP class: class-attendee-profile.php
- Helper functions for generating profile links
- Responsive CSS with mobile breakpoints
- JavaScript for timeline chart and interactions
- 6 comprehensive E2E tests added to final-working-tests.test.ts
- Fixed test locator ambiguity for "Email Attendee" text

## Test Results
All 6 attendee profile tests passing:
✓ Icon visibility in Generate Certificates
✓ Page accessibility and structure
✓ Direct access with parameters
✓ Chart rendering
✓ Timeline interactions
✓ Print functionality

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
bengizmo 2025-05-24 19:23:33 -03:00
parent 3d9599ef74
commit e74f064f4e
8 changed files with 1916 additions and 2 deletions

View file

@ -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();
}
}
});
});

View file

@ -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;
}
}

View file

@ -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);

View file

@ -87,6 +87,10 @@ function hvac_ce_create_required_pages() {
'title' => 'Trainer Documentation',
'content' => '<!-- wp:shortcode -->[hvac_documentation]<!-- /wp:shortcode -->',
],
'attendee-profile' => [ // Add attendee profile page
'title' => 'Attendee Profile',
'content' => '<!-- wp:shortcode -->[hvac_attendee_profile]<!-- /wp:shortcode -->',
],
// 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';

View file

@ -0,0 +1,422 @@
<?php
/**
* Attendee Profile Handler
*
* Handles the display and data retrieval for attendee profile pages
*
* @package HVAC_Community_Events
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Attendee Profile class
*/
class HVAC_Attendee_Profile {
/**
* Instance of this class
*
* @var HVAC_Attendee_Profile
*/
protected static $instance = null;
/**
* Get instance
*/
public static function instance() {
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
public function __construct() {
add_action('init', array($this, 'register_shortcode'));
add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
add_action('wp_ajax_hvac_get_attendee_timeline', array($this, 'ajax_get_timeline'));
add_action('wp_ajax_nopriv_hvac_get_attendee_timeline', array($this, 'ajax_get_timeline'));
}
/**
* Register shortcode
*/
public function register_shortcode() {
add_shortcode('hvac_attendee_profile', array($this, 'render_attendee_profile'));
}
/**
* Enqueue scripts and styles
*/
public function enqueue_scripts() {
if (!is_page('attendee-profile')) {
return;
}
// Enqueue Chart.js for timeline visualization
wp_enqueue_script(
'chartjs',
'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js',
array(),
'3.9.1',
true
);
// Enqueue custom styles and scripts
wp_enqueue_style(
'hvac-attendee-profile',
HVAC_CE_PLUGIN_URL . 'assets/css/hvac-attendee-profile.css',
array(),
HVAC_CE_VERSION
);
wp_enqueue_script(
'hvac-attendee-profile',
HVAC_CE_PLUGIN_URL . 'assets/js/hvac-attendee-profile.js',
array('jquery', 'chartjs'),
HVAC_CE_VERSION,
true
);
wp_localize_script('hvac-attendee-profile', 'hvac_attendee', array(
'ajax_url' => 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 '<p>You do not have permission to view attendee profiles.</p>';
}
// Get attendee ID from URL parameter
$attendee_id = isset($_GET['attendee_id']) ? intval($_GET['attendee_id']) : 0;
if (!$attendee_id) {
return '<p>No attendee specified. Please provide an attendee ID.</p>';
}
// Get attendee data
$attendee_data = $this->get_attendee_data($attendee_id);
if (!$attendee_data) {
return '<p>Attendee not found.</p>';
}
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();

View file

@ -0,0 +1,150 @@
<?php
/**
* Helper function to generate attendee profile links
*
* @package HVAC_Community_Events
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
/**
* Generate a link to view an attendee's profile
*
* @param int|object $attendee Attendee ID or attendee object
* @param string $link_text Optional custom link text
* @param array $classes Optional CSS classes
* @return string HTML link to attendee profile
*/
function hvac_get_attendee_profile_link($attendee, $link_text = '', $classes = array()) {
// 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));
// 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(
'<a href="%s" class="%s" target="_blank" title="%s">%s <i class="fas fa-external-link-alt"></i></a>',
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(
'<a href="%s" class="hvac-attendee-profile-icon" target="_blank" title="%s"><i class="fas fa-user-circle"></i></a>',
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() {
?>
<style>
.hvac-attendee-profile-link {
color: #007cba;
text-decoration: none;
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 5px;
}
.hvac-attendee-profile-link:hover {
text-decoration: underline;
}
.hvac-attendee-profile-link i {
font-size: 12px;
opacity: 0.7;
}
.hvac-attendee-profile-icon {
color: #007cba;
font-size: 18px;
text-decoration: none;
display: inline-block;
padding: 2px;
transition: color 0.2s ease;
}
.hvac-attendee-profile-icon:hover {
color: #005a87;
}
/* Add to table cells */
.hvac-attendee-name-cell {
display: flex;
align-items: center;
gap: 10px;
}
</style>
<?php
}
// Add styles to relevant pages
add_action('wp_head', function() {
if (is_page(array('hvac-dashboard', 'event-summary', 'certificate-reports', 'generate-certificates', 'email-attendees'))) {
hvac_attendee_profile_link_styles();
}
});

View file

@ -0,0 +1,222 @@
<?php
/**
* Template for Attendee Profile Page
*
* @package HVAC_Community_Events
* @since 1.0.0
*/
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
// Get data from parent scope
$profile = $attendee_data['profile'];
$stats = $attendee_data['stats'];
$timeline = $attendee_data['timeline'];
?>
<div class="hvac-attendee-profile">
<!-- Profile Header -->
<div class="hvac-profile-header">
<div class="hvac-profile-avatar">
<?php if (!empty($profile['avatar_url'])): ?>
<img src="<?php echo esc_url($profile['avatar_url']); ?>" alt="<?php echo esc_attr($profile['name']); ?>">
<?php else: ?>
<div class="hvac-avatar-placeholder">
<i class="fas fa-user-circle"></i>
</div>
<?php endif; ?>
</div>
<div class="hvac-profile-title">
<h1><?php echo esc_html($profile['name']); ?></h1>
<p class="hvac-profile-email"><?php echo esc_html($profile['email']); ?></p>
</div>
</div>
<!-- Statistics Section -->
<div class="hvac-stats-section">
<h2>
<i class="fas fa-chart-line"></i>
Activity Summary
</h2>
<div class="hvac-stats-grid">
<div class="hvac-stat-card">
<div class="hvac-stat-icon">
<i class="fas fa-shopping-cart"></i>
</div>
<div class="hvac-stat-content">
<div class="hvac-stat-value"><?php echo number_format($stats['total_purchases']); ?></div>
<div class="hvac-stat-label">Purchases Made</div>
</div>
</div>
<div class="hvac-stat-card">
<div class="hvac-stat-icon">
<i class="fas fa-calendar-alt"></i>
</div>
<div class="hvac-stat-content">
<div class="hvac-stat-value"><?php echo number_format($stats['events_registered']); ?></div>
<div class="hvac-stat-label">Events Registered</div>
</div>
</div>
<div class="hvac-stat-card">
<div class="hvac-stat-icon">
<i class="fas fa-check-circle"></i>
</div>
<div class="hvac-stat-content">
<div class="hvac-stat-value"><?php echo number_format($stats['events_checked_in']); ?></div>
<div class="hvac-stat-label">Events Attended</div>
</div>
</div>
<div class="hvac-stat-card">
<div class="hvac-stat-icon">
<i class="fas fa-certificate"></i>
</div>
<div class="hvac-stat-content">
<div class="hvac-stat-value"><?php echo number_format($stats['certificates_earned']); ?></div>
<div class="hvac-stat-label">Certificates Earned</div>
</div>
</div>
</div>
</div>
<!-- Profile Information Section -->
<div class="hvac-profile-section">
<h2>
<i class="fas fa-user"></i>
Profile Information
</h2>
<div class="hvac-profile-info">
<div class="hvac-info-row">
<div class="hvac-info-label">
<i class="fas fa-user"></i>
Name
</div>
<div class="hvac-info-value"><?php echo esc_html($profile['name']); ?></div>
</div>
<div class="hvac-info-row">
<div class="hvac-info-label">
<i class="fas fa-envelope"></i>
Email
</div>
<div class="hvac-info-value">
<a href="mailto:<?php echo esc_attr($profile['email']); ?>"><?php echo esc_html($profile['email']); ?></a>
</div>
</div>
<?php if (!empty($profile['phone'])): ?>
<div class="hvac-info-row">
<div class="hvac-info-label">
<i class="fas fa-phone"></i>
Phone
</div>
<div class="hvac-info-value">
<a href="tel:<?php echo esc_attr($profile['phone']); ?>"><?php echo esc_html($profile['phone']); ?></a>
</div>
</div>
<?php endif; ?>
<?php if (!empty($profile['company'])): ?>
<div class="hvac-info-row">
<div class="hvac-info-label">
<i class="fas fa-building"></i>
Company
</div>
<div class="hvac-info-value"><?php echo esc_html($profile['company']); ?></div>
</div>
<?php endif; ?>
<?php if (!empty($profile['state'])): ?>
<div class="hvac-info-row">
<div class="hvac-info-label">
<i class="fas fa-map-marker-alt"></i>
State
</div>
<div class="hvac-info-value"><?php echo esc_html($profile['state']); ?></div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Timeline Section -->
<div class="hvac-timeline-section">
<h2>
<i class="fas fa-history"></i>
Activity Timeline
</h2>
<?php if (empty($timeline)): ?>
<p class="hvac-no-activity">No activity recorded for this attendee.</p>
<?php else: ?>
<div class="hvac-timeline">
<?php foreach ($timeline as $index => $event): ?>
<div class="hvac-timeline-item" data-type="<?php echo esc_attr($event['type']); ?>">
<div class="hvac-timeline-date">
<?php echo date('M j, Y', strtotime($event['date'])); ?>
<span class="hvac-timeline-time"><?php echo date('g:i A', strtotime($event['date'])); ?></span>
</div>
<div class="hvac-timeline-marker" style="background-color: <?php echo esc_attr($event['color']); ?>">
<i class="<?php echo esc_attr($event['icon']); ?>"></i>
</div>
<div class="hvac-timeline-content">
<h4><?php echo esc_html($event['title']); ?></h4>
<?php if ($event['type'] === 'event' && isset($event['checked_in'])): ?>
<span class="hvac-checkin-status <?php echo $event['checked_in'] ? 'checked-in' : 'not-checked-in'; ?>">
<?php echo $event['checked_in'] ? 'Checked In' : 'Not Checked In'; ?>
</span>
<?php endif; ?>
<?php if ($event['type'] === 'certificate' && !empty($event['certificate_number'])): ?>
<span class="hvac-certificate-number">
Certificate #<?php echo esc_html($event['certificate_number']); ?>
</span>
<?php endif; ?>
<?php if (!empty($event['event_id'])): ?>
<a href="<?php echo esc_url(get_permalink($event['event_id'])); ?>" class="hvac-event-link" target="_blank">
View Event <i class="fas fa-external-link-alt"></i>
</a>
<?php endif; ?>
</div>
<?php if ($index < count($timeline) - 1): ?>
<div class="hvac-timeline-connector"></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- Timeline Chart -->
<div class="hvac-timeline-chart-section">
<h2>
<i class="fas fa-chart-bar"></i>
Activity Over Time
</h2>
<div class="hvac-chart-container">
<canvas id="hvac-timeline-chart"></canvas>
</div>
</div>
<!-- Action Buttons -->
<div class="hvac-profile-actions">
<a href="mailto:<?php echo esc_attr($profile['email']); ?>" class="hvac-button hvac-primary">
<i class="fas fa-envelope"></i> Email Attendee
</a>
<button class="hvac-button hvac-secondary" onclick="window.print()">
<i class="fas fa-print"></i> Print Profile
</button>
<a href="<?php echo esc_url(wp_get_referer() ?: home_url('/hvac-dashboard/')); ?>" class="hvac-button hvac-ghost">
<i class="fas fa-arrow-left"></i> Back
</a>
</div>
</div>
<script>
// Pass timeline data to JavaScript
var hvacTimelineData = <?php echo json_encode($timeline); ?>;
</script>

View file

@ -213,7 +213,10 @@ get_header();
<span class="hvac-certificate-exists" title="Certificate already generated"></span>
<?php endif; ?>
</td>
<td><?php echo esc_html($attendee_name); ?></td>
<td class="hvac-attendee-name-cell">
<?php echo esc_html($attendee_name); ?>
<?php echo hvac_get_attendee_profile_icon($attendee); ?>
</td>
<td><?php echo esc_html($attendee_email); ?></td>
<td>
<span class="<?php echo esc_attr($status_class); ?>">