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:
parent
3d9599ef74
commit
e74f064f4e
8 changed files with 1916 additions and 2 deletions
|
|
@ -182,4 +182,282 @@ test.describe('HVAC Plugin - Final Working Tests', () => {
|
||||||
expect(criticalErrors.length).toBe(0);
|
expect(criticalErrors.length).toBe(0);
|
||||||
console.log('✓ No critical errors found');
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -87,6 +87,10 @@ function hvac_ce_create_required_pages() {
|
||||||
'title' => 'Trainer Documentation',
|
'title' => 'Trainer Documentation',
|
||||||
'content' => '<!-- wp:shortcode -->[hvac_documentation]<!-- /wp:shortcode -->',
|
'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.
|
// REMOVED: 'submit-event' page creation. Will link to default TEC CE page.
|
||||||
// 'submit-event' => [
|
// 'submit-event' => [
|
||||||
// 'title' => 'Submit Event',
|
// 'title' => 'Submit Event',
|
||||||
|
|
@ -229,7 +233,7 @@ function hvac_ce_enqueue_common_assets() {
|
||||||
$hvac_pages = [
|
$hvac_pages = [
|
||||||
'hvac-dashboard', 'community-login', 'trainer-registration', 'trainer-profile',
|
'hvac-dashboard', 'community-login', 'trainer-registration', 'trainer-profile',
|
||||||
'manage-event', 'event-summary', 'email-attendees', 'certificate-reports',
|
'manage-event', 'event-summary', 'email-attendees', 'certificate-reports',
|
||||||
'generate-certificates', 'certificate-fix', 'hvac-documentation'
|
'generate-certificates', 'certificate-fix', 'hvac-documentation', 'attendee-profile'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Only proceed if we're on an HVAC page
|
// 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
|
// Removed - template handling is now in the main class
|
||||||
// add_filter( 'template_include', 'hvac_ce_include_event_summary_template', 99 );
|
// 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';
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -213,7 +213,10 @@ get_header();
|
||||||
<span class="hvac-certificate-exists" title="Certificate already generated">✓</span>
|
<span class="hvac-certificate-exists" title="Certificate already generated">✓</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</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><?php echo esc_html($attendee_email); ?></td>
|
||||||
<td>
|
<td>
|
||||||
<span class="<?php echo esc_attr($status_class); ?>">
|
<span class="<?php echo esc_attr($status_class); ?>">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue