feat: complete Phase 2B template system enhancements
✨ Enhanced Template Selector: - Grouped templates by category with descriptions - Template preview modal with field data display - Apply template functionality with AJAX loading - Enhanced UI with preview icons and better UX ✨ Save as Template Functionality: - Complete save template dialog with validation - Template name, description, and category fields - Public/private template sharing options - AJAX integration with error handling and success feedback ✨ Progressive Disclosure: - Advanced options toggle with smooth animations - Fields marked as advanced (capacity, cost, timezone) - Local storage for user preference persistence - Staggered reveal animations for better UX ✨ Enhanced Auto-save: - Intelligent auto-save with field-type specific delays - Draft recovery with age information and user confirmation - Error handling with fallback to essential fields only - Visual feedback with status indicator and animations - Auto-save on page visibility change ✨ AJAX Infrastructure: - Template preview handler (hvac_get_template_preview) - Template loading handler (hvac_load_template_data) - Template saving handler (hvac_save_template) - Comprehensive error handling and security validation 🎨 UI/UX Enhancements: - Modern modal dialogs with backdrop overlays - Responsive design for mobile devices - Smooth animations and transitions - Status indicators with rotating save icons - Comprehensive styling for all new components 🚀 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ac8c09a294
commit
09a15f874c
5 changed files with 1856 additions and 54 deletions
|
|
@ -21,7 +21,10 @@
|
||||||
"WebFetch(domain:upskill-staging.measurequick.com)",
|
"WebFetch(domain:upskill-staging.measurequick.com)",
|
||||||
"Bash(scripts/:*)",
|
"Bash(scripts/:*)",
|
||||||
"Bash(./scripts/:*)",
|
"Bash(./scripts/:*)",
|
||||||
"Bash(php -l:*)"
|
"Bash(php -l:*)",
|
||||||
|
"Bash(SSHPASS=\"uSCO6f1y\" sshpass -e scp -o StrictHostKeyChecking=no templates/page-native-event-test.php roodev@146.190.76.204:/home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/plugins/hvac-community-events/templates/)",
|
||||||
|
"Bash(SSHPASS=\"uSCO6f1y\" sshpass -e scp -o StrictHostKeyChecking=no templates/page-create-event.php roodev@146.190.76.204:/home/974670.cloudwaysapps.com/uberrxmprk/public_html/wp-content/plugins/hvac-community-events/templates/)",
|
||||||
|
"mcp__playwright__browser_navigate"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": [],
|
"ask": [],
|
||||||
|
|
|
||||||
492
docs/FEATURE_AI_EVENT_POPULATION.md
Normal file
492
docs/FEATURE_AI_EVENT_POPULATION.md
Normal file
|
|
@ -0,0 +1,492 @@
|
||||||
|
# AI-Assisted Event Submission Feature Specification
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This document specifies an AI-powered feature to reduce friction for HVAC trainers submitting events to a WordPress calendar system. The solution uses a single Anthropic Claude API call to parse unstructured event information and auto-populate form fields, dramatically simplifying the submission process for non-technical users.
|
||||||
|
|
||||||
|
## Project Context
|
||||||
|
|
||||||
|
### Current State
|
||||||
|
- **Platform**: WordPress site hosted on Cloudways
|
||||||
|
- **Base Plugin**: The Events Calendar (TEC) by Modern Tribe
|
||||||
|
- **Custom Plugin**: Existing custom plugin built on top of TEC for HVAC trainer event submissions
|
||||||
|
- **User Base**: HVAC trainers who are often not tech-savvy and have limited time
|
||||||
|
- **Problem**: High friction in manual form entry leading to incomplete or missing event submissions
|
||||||
|
|
||||||
|
### Technical Constraints
|
||||||
|
- **Hosting**: Cloudways shared hosting environment
|
||||||
|
- Limited server-side processing capabilities
|
||||||
|
- No ability to install additional server software
|
||||||
|
- Standard PHP/MySQL stack
|
||||||
|
- HTTPS request capabilities
|
||||||
|
- **Must maintain compatibility** with The Events Calendar plugin data structures
|
||||||
|
- **Cannot add additional WordPress plugins** - solution must be implemented within existing custom plugin
|
||||||
|
|
||||||
|
## Functional Requirements
|
||||||
|
|
||||||
|
### Core Feature: AI-Assisted Event Form Population
|
||||||
|
|
||||||
|
#### User Flow
|
||||||
|
1. User navigates to the create event page
|
||||||
|
2. User sees an "Auto-populate with AI" button/option
|
||||||
|
3. Upon clicking, a modal dialog appears with:
|
||||||
|
- Clear instructions for the user
|
||||||
|
- Single rich text input field
|
||||||
|
- Submit button
|
||||||
|
4. User provides one of three input types:
|
||||||
|
- **Option A**: URL to a webpage containing event details
|
||||||
|
- **Option B**: Copy/pasted text from email/document
|
||||||
|
- **Option C**: Brief description with key details
|
||||||
|
5. System processes input showing:
|
||||||
|
- Loading spinner
|
||||||
|
- Status messages during processing
|
||||||
|
6. System populates form fields automatically
|
||||||
|
7. System displays confidence/completeness matrix
|
||||||
|
8. User reviews and adjusts populated fields before final submission
|
||||||
|
|
||||||
|
#### Input Acceptance Criteria
|
||||||
|
|
||||||
|
**URL Input**
|
||||||
|
- Accept any valid HTTP/HTTPS URL
|
||||||
|
- Handle various event page formats (EventBrite, custom sites, social media event pages)
|
||||||
|
- Extract information even from non-standard page structures
|
||||||
|
|
||||||
|
**Pasted Text Input**
|
||||||
|
- Accept unformatted text from emails, PDFs, documents
|
||||||
|
- Handle various formatting styles and structures
|
||||||
|
- Parse information regardless of order or format
|
||||||
|
|
||||||
|
**Brief Description Input**
|
||||||
|
- Accept natural language descriptions
|
||||||
|
- Extract partial information when complete details aren't provided
|
||||||
|
- Make intelligent assumptions about missing data
|
||||||
|
|
||||||
|
#### Output Requirements
|
||||||
|
|
||||||
|
**Required Fields to Extract**
|
||||||
|
- Event title
|
||||||
|
- Date and time
|
||||||
|
- Location/venue
|
||||||
|
- Description
|
||||||
|
- Organizer
|
||||||
|
- Cost/pricing
|
||||||
|
- Registration URL (if available)
|
||||||
|
|
||||||
|
**Data Validation Requirements**
|
||||||
|
- Check against existing venues to avoid duplicates
|
||||||
|
- Check against existing organizers to avoid duplicates
|
||||||
|
- Validate date/time formats
|
||||||
|
- Ensure required fields are populated
|
||||||
|
|
||||||
|
**Confidence Reporting**
|
||||||
|
Display a matrix showing:
|
||||||
|
- **Completeness**: Whether each field was found (High/Medium/Low/N/A)
|
||||||
|
- **Confidence**: How confident the AI is in the extracted data (0.0-1.0)
|
||||||
|
|
||||||
|
## Technical Architecture
|
||||||
|
|
||||||
|
### Simplified Single-API Approach
|
||||||
|
|
||||||
|
#### Architecture Decision
|
||||||
|
Use a single Anthropic Claude API call with web search capabilities enabled, eliminating the need for:
|
||||||
|
- Separate web scraping services
|
||||||
|
- Multiple NLP processing steps
|
||||||
|
- Complex orchestration logic
|
||||||
|
- Background job processing
|
||||||
|
- Additional server infrastructure
|
||||||
|
|
||||||
|
#### Key Components
|
||||||
|
|
||||||
|
1. **Frontend (JavaScript)**
|
||||||
|
- Modal interface for input collection
|
||||||
|
- AJAX communication with WordPress backend
|
||||||
|
- Form field population logic
|
||||||
|
- Confidence matrix display
|
||||||
|
|
||||||
|
2. **Backend (PHP/WordPress)**
|
||||||
|
- Single AJAX endpoint for AI processing
|
||||||
|
- API key management
|
||||||
|
- Request/response handling
|
||||||
|
- Data sanitization and validation
|
||||||
|
|
||||||
|
3. **External Service**
|
||||||
|
- Anthropic Claude API (claude-3-5-sonnet-20241022 or latest)
|
||||||
|
- Web search/browsing capabilities enabled
|
||||||
|
|
||||||
|
### Implementation Patterns
|
||||||
|
|
||||||
|
#### Pattern 1: Stateless Request Processing
|
||||||
|
- **Do**: Make each request completely self-contained
|
||||||
|
- **Do**: Include all context needed in single prompt
|
||||||
|
- **Do**: Return complete results in one response
|
||||||
|
- **Don't**: Maintain conversation state between requests
|
||||||
|
- **Don't**: Make multiple API calls for single submission
|
||||||
|
|
||||||
|
#### Pattern 2: Defensive Data Handling
|
||||||
|
```php
|
||||||
|
// Always validate and sanitize
|
||||||
|
$user_input = sanitize_textarea_field($_POST['input'] ?? '');
|
||||||
|
if (empty($user_input)) {
|
||||||
|
wp_send_json_error(['message' => 'No input provided']);
|
||||||
|
wp_die();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pattern 3: Structured Output Enforcement
|
||||||
|
- **Do**: Request JSON output explicitly in prompt
|
||||||
|
- **Do**: Provide exact schema in prompt
|
||||||
|
- **Do**: Include example of expected output
|
||||||
|
- **Don't**: Parse free-text responses
|
||||||
|
- **Don't**: Rely on consistent formatting without schema
|
||||||
|
|
||||||
|
#### Pattern 4: Graceful Degradation
|
||||||
|
- **Do**: Provide fallback for API failures
|
||||||
|
- **Do**: Allow manual form filling if AI fails
|
||||||
|
- **Do**: Cache successful extractions
|
||||||
|
- **Don't**: Block user progress on AI failure
|
||||||
|
- **Don't**: Require AI for form submission
|
||||||
|
|
||||||
|
### Anti-Patterns to Avoid
|
||||||
|
|
||||||
|
#### Anti-Pattern 1: Client-Side API Keys
|
||||||
|
**Never** expose API keys in JavaScript or make direct API calls from browser
|
||||||
|
|
||||||
|
#### Anti-Pattern 2: Synchronous Long-Running Requests
|
||||||
|
**Avoid** blocking UI during API calls; always use async patterns with loading states
|
||||||
|
|
||||||
|
#### Anti-Pattern 3: Over-Engineering
|
||||||
|
**Don't** build complex parsing systems when Claude can handle it in one call
|
||||||
|
|
||||||
|
#### Anti-Pattern 4: Rigid Field Matching
|
||||||
|
**Don't** require exact field matches; use fuzzy matching for venues/organizers
|
||||||
|
|
||||||
|
## Prompt Engineering Strategy
|
||||||
|
|
||||||
|
### Core Prompt Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
You are an AI assistant specialized in extracting event information from various sources.
|
||||||
|
Your task is to extract structured event data for an HVAC training calendar.
|
||||||
|
|
||||||
|
[CONTEXT]
|
||||||
|
- These are professional training events for HVAC technicians
|
||||||
|
- Events may be in-person or virtual
|
||||||
|
- Common organizers include: {list_of_existing_organizers}
|
||||||
|
- Common venues include: {list_of_existing_venues}
|
||||||
|
|
||||||
|
[INPUT]
|
||||||
|
{user_provides_one_of_these}
|
||||||
|
- URL: Please retrieve and analyze the webpage at: {url}
|
||||||
|
- Text: Parse the following event information: {pasted_text}
|
||||||
|
- Description: Extract event details from: {brief_description}
|
||||||
|
|
||||||
|
[EXTRACTION RULES]
|
||||||
|
1. Match venues/organizers to existing ones when similarity > 80%
|
||||||
|
2. Infer missing information when reasonable (mark as low confidence)
|
||||||
|
3. Handle relative dates (convert "next Tuesday" to actual date)
|
||||||
|
4. Extract all URLs found in source material
|
||||||
|
5. Standardize time to 24-hour format
|
||||||
|
6. If multiple events found, extract only the first/primary one
|
||||||
|
|
||||||
|
[OUTPUT SCHEMA]
|
||||||
|
Return ONLY a valid JSON object with this exact structure:
|
||||||
|
{json_schema_here}
|
||||||
|
|
||||||
|
[IMPORTANT]
|
||||||
|
- Return ONLY the JSON object, no explanatory text
|
||||||
|
- Use null for missing fields rather than empty strings
|
||||||
|
- Include confidence scores for each field
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prompt Optimization Techniques
|
||||||
|
|
||||||
|
#### 1. Few-Shot Examples
|
||||||
|
Include 2-3 examples of successful extractions in the system prompt to improve consistency.
|
||||||
|
|
||||||
|
#### 2. Role Definition
|
||||||
|
Clearly define Claude's role as an "event information extraction specialist" to improve focus.
|
||||||
|
|
||||||
|
#### 3. Explicit Constraints
|
||||||
|
- Specify exact date format (ISO 8601)
|
||||||
|
- Define confidence score ranges (0.0-1.0)
|
||||||
|
- List acceptable values for enums
|
||||||
|
|
||||||
|
#### 4. Context Injection
|
||||||
|
Always include:
|
||||||
|
- Current date/time for relative date resolution
|
||||||
|
- List of existing venues/organizers
|
||||||
|
- Common patterns in your specific domain
|
||||||
|
|
||||||
|
### Handling Edge Cases
|
||||||
|
|
||||||
|
#### Edge Case 1: Multiple Events
|
||||||
|
**Strategy**: Extract only the first/most prominent event
|
||||||
|
**Prompt Addition**: "If multiple events are found, extract only the primary/first event"
|
||||||
|
|
||||||
|
#### Edge Case 2: Ambiguous Dates
|
||||||
|
**Strategy**: Request current date context
|
||||||
|
**Implementation**: Always include `Today is {current_date}` in prompt
|
||||||
|
|
||||||
|
#### Edge Case 3: Missing Critical Information
|
||||||
|
**Strategy**: Return partial data with low confidence
|
||||||
|
**Validation**: Check for minimum required fields before population
|
||||||
|
|
||||||
|
## Input/Output Validation
|
||||||
|
|
||||||
|
### Input Validation Strategy
|
||||||
|
|
||||||
|
#### Pre-Processing Validation
|
||||||
|
```javascript
|
||||||
|
function validateInput(input) {
|
||||||
|
// Check for minimum length
|
||||||
|
if (input.length < 10) {
|
||||||
|
return { valid: false, error: 'Input too short' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect input type
|
||||||
|
const type = detectInputType(input);
|
||||||
|
|
||||||
|
// URL validation
|
||||||
|
if (type === 'url') {
|
||||||
|
try {
|
||||||
|
new URL(input);
|
||||||
|
} catch {
|
||||||
|
return { valid: false, error: 'Invalid URL format' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size limits (prevent token overflow)
|
||||||
|
if (input.length > 50000) {
|
||||||
|
return { valid: false, error: 'Input too large' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true, type };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Input Sanitization
|
||||||
|
- Strip potentially harmful HTML/scripts
|
||||||
|
- Normalize whitespace and line breaks
|
||||||
|
- Remove non-printable characters
|
||||||
|
- Truncate to maximum acceptable length
|
||||||
|
|
||||||
|
### Output Validation Strategy
|
||||||
|
|
||||||
|
#### Schema Validation
|
||||||
|
```php
|
||||||
|
function validate_ai_response($response) {
|
||||||
|
$required_fields = ['title', 'date'];
|
||||||
|
$schema = [
|
||||||
|
'title' => 'string',
|
||||||
|
'date' => 'date',
|
||||||
|
'venue' => 'string|null',
|
||||||
|
'confidence' => 'array',
|
||||||
|
'completeness' => 'array'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check JSON structure
|
||||||
|
if (!is_array($response)) {
|
||||||
|
throw new ValidationException('Invalid JSON response');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
foreach ($required_fields as $field) {
|
||||||
|
if (empty($response[$field])) {
|
||||||
|
throw new ValidationException("Missing required field: {$field}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type checking and sanitization
|
||||||
|
// ... implementation details
|
||||||
|
|
||||||
|
return $sanitized_response;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Confidence Threshold Handling
|
||||||
|
- **High Confidence (>0.8)**: Auto-populate without warning
|
||||||
|
- **Medium Confidence (0.5-0.8)**: Populate with visual indicator
|
||||||
|
- **Low Confidence (<0.5)**: Populate but highlight for review
|
||||||
|
- **No Confidence (null)**: Leave field empty
|
||||||
|
|
||||||
|
#### Duplicate Detection
|
||||||
|
```php
|
||||||
|
function check_duplicate_event($event_data) {
|
||||||
|
// Check for same date + similar title
|
||||||
|
$similar_events = get_posts([
|
||||||
|
'post_type' => 'tribe_events',
|
||||||
|
'meta_key' => '_EventStartDate',
|
||||||
|
'meta_value' => $event_data['date'],
|
||||||
|
'meta_compare' => '='
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($similar_events as $existing) {
|
||||||
|
$similarity = similar_text(
|
||||||
|
strtolower($existing->post_title),
|
||||||
|
strtolower($event_data['title']),
|
||||||
|
$percent
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($percent > 75) {
|
||||||
|
return [
|
||||||
|
'is_duplicate' => true,
|
||||||
|
'existing_id' => $existing->ID,
|
||||||
|
'similarity' => $percent
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['is_duplicate' => false];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling and Recovery
|
||||||
|
|
||||||
|
### API Error Handling
|
||||||
|
|
||||||
|
#### Rate Limiting
|
||||||
|
- Implement exponential backoff
|
||||||
|
- Cache successful responses for 24 hours
|
||||||
|
- Show user-friendly message when limit reached
|
||||||
|
|
||||||
|
#### Network Failures
|
||||||
|
- Timeout after 30 seconds
|
||||||
|
- Provide option to retry
|
||||||
|
- Fall back to manual entry
|
||||||
|
|
||||||
|
#### Invalid Responses
|
||||||
|
- Log malformed responses for debugging
|
||||||
|
- Show generic error to user
|
||||||
|
- Provide option to try different input
|
||||||
|
|
||||||
|
### User Communication
|
||||||
|
|
||||||
|
#### Status Messages
|
||||||
|
1. "Analyzing your input..." (0-2 seconds)
|
||||||
|
2. "Extracting event information..." (2-10 seconds)
|
||||||
|
3. "Validating extracted data..." (10-15 seconds)
|
||||||
|
4. "Populating form fields..." (15+ seconds)
|
||||||
|
|
||||||
|
#### Error Messages
|
||||||
|
- Be specific but not technical
|
||||||
|
- Provide actionable next steps
|
||||||
|
- Never expose internal errors or API keys
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### API Key Management
|
||||||
|
- Store in wp-config.php or environment variables
|
||||||
|
- Never commit to version control
|
||||||
|
- Rotate keys periodically
|
||||||
|
- Use WordPress nonce for AJAX calls
|
||||||
|
|
||||||
|
### Input Security
|
||||||
|
- Sanitize all user inputs
|
||||||
|
- Prevent SQL injection via parameterized queries
|
||||||
|
- Escape output when displaying
|
||||||
|
- Limit request frequency per user
|
||||||
|
|
||||||
|
### Data Privacy
|
||||||
|
- Don't log sensitive event information
|
||||||
|
- Clear temporary data after processing
|
||||||
|
- Comply with GDPR/privacy requirements
|
||||||
|
- Allow users to opt-out of AI features
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Caching Strategy
|
||||||
|
- Cache AI responses for identical inputs (24-hour TTL)
|
||||||
|
- Cache venue/organizer lists (1-hour TTL)
|
||||||
|
- Use WordPress transients for temporary data
|
||||||
|
|
||||||
|
### Request Optimization
|
||||||
|
- Minimize prompt size while maintaining clarity
|
||||||
|
- Pre-process input to remove redundant information
|
||||||
|
- Batch similar requests when possible
|
||||||
|
|
||||||
|
### Frontend Optimization
|
||||||
|
- Lazy-load AI modal components
|
||||||
|
- Debounce input validation
|
||||||
|
- Use optimistic UI updates
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Test Scenarios
|
||||||
|
1. **URL Input**: Test with EventBrite, Facebook Events, custom sites
|
||||||
|
2. **Text Input**: Test with various email formats, PDF copies
|
||||||
|
3. **Description Input**: Test with minimal and detailed descriptions
|
||||||
|
4. **Edge Cases**: Test with non-English content, past events, recurring events
|
||||||
|
5. **Failure Cases**: Test with invalid URLs, gibberish input, timeout scenarios
|
||||||
|
|
||||||
|
### Validation Testing
|
||||||
|
- Ensure all required fields are validated
|
||||||
|
- Test confidence score accuracy
|
||||||
|
- Verify duplicate detection works correctly
|
||||||
|
- Test venue/organizer matching logic
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
### Phase 1: Core Infrastructure
|
||||||
|
- [ ] Set up Anthropic API integration
|
||||||
|
- [ ] Create WordPress AJAX endpoint
|
||||||
|
- [ ] Implement basic input validation
|
||||||
|
- [ ] Build JSON response parser
|
||||||
|
|
||||||
|
### Phase 2: UI Implementation
|
||||||
|
- [ ] Create modal interface
|
||||||
|
- [ ] Implement form field population
|
||||||
|
- [ ] Add loading states and progress indicators
|
||||||
|
- [ ] Build confidence matrix display
|
||||||
|
|
||||||
|
### Phase 3: Data Processing
|
||||||
|
- [ ] Implement venue/organizer matching
|
||||||
|
- [ ] Add duplicate detection
|
||||||
|
- [ ] Create validation pipeline
|
||||||
|
- [ ] Add error handling and recovery
|
||||||
|
|
||||||
|
### Phase 4: Optimization
|
||||||
|
- [ ] Implement caching layer
|
||||||
|
- [ ] Add request rate limiting
|
||||||
|
- [ ] Optimize prompt for accuracy
|
||||||
|
- [ ] Performance testing and tuning
|
||||||
|
|
||||||
|
### Phase 5: Polish and Testing
|
||||||
|
- [ ] Comprehensive error messages
|
||||||
|
- [ ] User documentation
|
||||||
|
- [ ] Edge case handling
|
||||||
|
- [ ] Security audit
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
### Technical Metrics
|
||||||
|
- API response time < 10 seconds (95th percentile)
|
||||||
|
- Extraction accuracy > 85% for standard inputs
|
||||||
|
- Zero security vulnerabilities
|
||||||
|
- 99.9% uptime for feature availability
|
||||||
|
|
||||||
|
### User Metrics
|
||||||
|
- 50% reduction in time to submit events
|
||||||
|
- 75% of submissions use AI assistance
|
||||||
|
- < 5% error rate requiring manual intervention
|
||||||
|
- User satisfaction score > 4/5
|
||||||
|
|
||||||
|
## Appendix: The Events Calendar Integration
|
||||||
|
|
||||||
|
### Relevant Post Types
|
||||||
|
- `tribe_events` - Main event post type
|
||||||
|
- `tribe_venue` - Venue post type
|
||||||
|
- `tribe_organizer` - Organizer post type
|
||||||
|
|
||||||
|
### Key Meta Fields
|
||||||
|
- `_EventStartDate` - Event start date/time
|
||||||
|
- `_EventEndDate` - Event end date/time
|
||||||
|
- `_EventVenueID` - Reference to venue post
|
||||||
|
- `_EventOrganizerID` - Reference to organizer post
|
||||||
|
- `_EventCost` - Event cost
|
||||||
|
- `_EventURL` - Registration/info URL
|
||||||
|
|
||||||
|
### Integration Points
|
||||||
|
- Use TEC's built-in functions when available
|
||||||
|
- Respect TEC's data validation rules
|
||||||
|
- Maintain compatibility with TEC updates
|
||||||
|
- Leverage TEC's duplicate detection if available
|
||||||
|
|
@ -190,9 +190,19 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder {
|
||||||
$this->add_cost_fields();
|
$this->add_cost_fields();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add progressive disclosure toggle
|
||||||
|
$this->add_progressive_disclosure();
|
||||||
|
|
||||||
|
// Mark certain fields as advanced
|
||||||
|
$this->mark_field_as_advanced('event_capacity')
|
||||||
|
->mark_field_as_advanced('event_cost')
|
||||||
|
->mark_field_as_advanced('event_timezone');
|
||||||
|
|
||||||
// Template actions if enabled
|
// Template actions if enabled
|
||||||
if ($this->template_mode_enabled) {
|
if ($this->template_mode_enabled) {
|
||||||
$this->add_template_actions();
|
$this->add_template_actions();
|
||||||
|
// Mark template actions as advanced
|
||||||
|
$this->mark_field_as_advanced('save_as_template');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
|
|
@ -220,23 +230,55 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder {
|
||||||
$templates = $this->template_manager->get_templates();
|
$templates = $this->template_manager->get_templates();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare template options
|
// Group templates by category for enhanced UI
|
||||||
$template_options = ['0' => '-- Select a Template --'];
|
$templates_by_category = [];
|
||||||
foreach ($templates as $template) {
|
foreach ($templates as $template) {
|
||||||
$template_options[$template['id']] = esc_html($template['name']) .
|
$category = $template['category'] ?? 'general';
|
||||||
' (' . ucfirst($template['category']) . ')';
|
if (!isset($templates_by_category[$category])) {
|
||||||
|
$templates_by_category[$category] = [];
|
||||||
|
}
|
||||||
|
$templates_by_category[$category][] = $template;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare enhanced template options with categories
|
||||||
|
$template_options = ['0' => '-- Select a Template --'];
|
||||||
|
foreach ($templates_by_category as $category => $category_templates) {
|
||||||
|
$template_options['optgroup_' . $category] = [
|
||||||
|
'label' => ucfirst($category) . ' Templates',
|
||||||
|
'options' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($category_templates as $template) {
|
||||||
|
$template_options['optgroup_' . $category]['options'][$template['id']] =
|
||||||
|
esc_html($template['name']) .
|
||||||
|
(!empty($template['description']) ? ' - ' . wp_trim_words($template['description'], 8) : '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$template_field = array_merge($this->event_field_defaults['template-selector'], [
|
$template_field = array_merge($this->event_field_defaults['template-selector'], [
|
||||||
'name' => 'event_template',
|
'name' => 'event_template',
|
||||||
'label' => 'Use Template',
|
'label' => 'Use Template',
|
||||||
'options' => $template_options,
|
'options' => $template_options,
|
||||||
'description' => 'Select a template to pre-fill form fields',
|
'description' => 'Select a template to pre-fill form fields. Templates are organized by category.',
|
||||||
'wrapper_class' => 'form-row template-selector-row',
|
'wrapper_class' => 'form-row template-selector-row enhanced-selector',
|
||||||
|
'data_attributes' => [
|
||||||
|
'templates' => json_encode($templates),
|
||||||
|
'enable_preview' => 'true'
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->add_field($template_field);
|
$this->add_field($template_field);
|
||||||
|
|
||||||
|
// Add template preview area
|
||||||
|
$preview_field = [
|
||||||
|
'type' => 'custom',
|
||||||
|
'name' => 'template_preview',
|
||||||
|
'custom_html' => $this->render_template_preview_area(),
|
||||||
|
'wrapper_class' => 'form-row template-preview-row',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->add_field($preview_field);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -401,14 +443,73 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder {
|
||||||
'value' => 'Save as Template',
|
'value' => 'Save as Template',
|
||||||
'class' => 'button button-secondary hvac-save-template',
|
'class' => 'button button-secondary hvac-save-template',
|
||||||
'wrapper_class' => 'form-row template-actions',
|
'wrapper_class' => 'form-row template-actions',
|
||||||
'onclick' => 'hvacSaveAsTemplate(event)',
|
'onclick' => 'hvacShowSaveTemplateDialog(event)',
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->add_field($save_template_field);
|
$this->add_field($save_template_field);
|
||||||
|
|
||||||
|
// Add save template dialog
|
||||||
|
$save_dialog_field = [
|
||||||
|
'type' => 'custom',
|
||||||
|
'name' => 'save_template_dialog',
|
||||||
|
'custom_html' => $this->render_save_template_dialog(),
|
||||||
|
'wrapper_class' => 'form-row template-dialog-row',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->add_field($save_dialog_field);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add progressive disclosure section
|
||||||
|
*/
|
||||||
|
public function add_progressive_disclosure(): self {
|
||||||
|
// Advanced options toggle
|
||||||
|
$advanced_toggle_field = [
|
||||||
|
'type' => 'custom',
|
||||||
|
'name' => 'advanced_options_toggle',
|
||||||
|
'custom_html' => $this->render_advanced_options_toggle(),
|
||||||
|
'wrapper_class' => 'form-row advanced-toggle-row',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->add_field($advanced_toggle_field);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark field as advanced (for progressive disclosure)
|
||||||
|
*/
|
||||||
|
public function mark_field_as_advanced(string $field_name): self {
|
||||||
|
foreach ($this->fields as &$field) {
|
||||||
|
if ($field['name'] === $field_name) {
|
||||||
|
$field['wrapper_class'] = ($field['wrapper_class'] ?? '') . ' advanced-field';
|
||||||
|
$field['data_attributes'] = array_merge($field['data_attributes'] ?? [], [
|
||||||
|
'advanced' => 'true'
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render advanced options toggle button
|
||||||
|
*/
|
||||||
|
private function render_advanced_options_toggle(): string {
|
||||||
|
$html = '<div class="hvac-advanced-options-toggle">';
|
||||||
|
$html .= '<button type="button" class="toggle-advanced-options" onclick="hvacToggleAdvancedOptions()">';
|
||||||
|
$html .= '<span class="dashicons dashicons-arrow-down-alt2 toggle-icon"></span>';
|
||||||
|
$html .= '<span class="toggle-text">Show Advanced Options</span>';
|
||||||
|
$html .= '</button>';
|
||||||
|
$html .= '<small class="toggle-description">Additional settings for power users</small>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load template data into form
|
* Load template data into form
|
||||||
*
|
*
|
||||||
|
|
@ -499,6 +600,97 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render template preview area
|
||||||
|
*
|
||||||
|
* @return string Preview HTML
|
||||||
|
*/
|
||||||
|
private function render_template_preview_area(): string {
|
||||||
|
$html = '<div class="hvac-template-preview" id="hvac-template-preview" style="display: none;">';
|
||||||
|
$html .= '<div class="preview-header">';
|
||||||
|
$html .= '<h4><span class="dashicons dashicons-visibility"></span> Template Preview</h4>';
|
||||||
|
$html .= '<button type="button" class="preview-close" onclick="hvacCloseTemplatePreview()">×</button>';
|
||||||
|
$html .= '</div>';
|
||||||
|
$html .= '<div class="preview-content">';
|
||||||
|
$html .= '<div class="preview-info">';
|
||||||
|
$html .= '<p class="template-name"><strong></strong></p>';
|
||||||
|
$html .= '<p class="template-description"></p>';
|
||||||
|
$html .= '<p class="template-category">Category: <span></span></p>';
|
||||||
|
$html .= '</div>';
|
||||||
|
$html .= '<div class="preview-fields">';
|
||||||
|
$html .= '<h5>Pre-filled Fields:</h5>';
|
||||||
|
$html .= '<ul class="field-list"></ul>';
|
||||||
|
$html .= '</div>';
|
||||||
|
$html .= '<div class="preview-actions">';
|
||||||
|
$html .= '<button type="button" class="button button-primary" onclick="hvacApplyTemplate()">Apply This Template</button>';
|
||||||
|
$html .= '<button type="button" class="button" onclick="hvacCloseTemplatePreview()">Cancel</button>';
|
||||||
|
$html .= '</div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render save template dialog
|
||||||
|
*
|
||||||
|
* @return string Dialog HTML
|
||||||
|
*/
|
||||||
|
private function render_save_template_dialog(): string {
|
||||||
|
$html = '<div class="hvac-save-template-dialog" id="hvac-save-template-dialog" style="display: none;">';
|
||||||
|
$html .= '<div class="dialog-header">';
|
||||||
|
$html .= '<h4><span class="dashicons dashicons-saved"></span> Save as Template</h4>';
|
||||||
|
$html .= '<button type="button" class="dialog-close" onclick="hvacCloseSaveDialog()">×</button>';
|
||||||
|
$html .= '</div>';
|
||||||
|
$html .= '<div class="dialog-content">';
|
||||||
|
|
||||||
|
// Template name field
|
||||||
|
$html .= '<div class="form-group">';
|
||||||
|
$html .= '<label for="template-name"><strong>Template Name *</strong></label>';
|
||||||
|
$html .= '<input type="text" id="template-name" name="template_name" required maxlength="100" ';
|
||||||
|
$html .= 'placeholder="Enter template name..." class="template-input">';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
// Template description field
|
||||||
|
$html .= '<div class="form-group">';
|
||||||
|
$html .= '<label for="template-description"><strong>Description</strong></label>';
|
||||||
|
$html .= '<textarea id="template-description" name="template_description" rows="3" maxlength="500" ';
|
||||||
|
$html .= 'placeholder="Brief description of this template..." class="template-input"></textarea>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
// Template category field
|
||||||
|
$html .= '<div class="form-group">';
|
||||||
|
$html .= '<label for="template-category"><strong>Category *</strong></label>';
|
||||||
|
$html .= '<select id="template-category" name="template_category" required class="template-input">';
|
||||||
|
$html .= '<option value="">Select Category...</option>';
|
||||||
|
$html .= '<option value="general">General</option>';
|
||||||
|
$html .= '<option value="training">Training</option>';
|
||||||
|
$html .= '<option value="workshop">Workshop</option>';
|
||||||
|
$html .= '<option value="certification">Certification</option>';
|
||||||
|
$html .= '<option value="conference">Conference</option>';
|
||||||
|
$html .= '<option value="webinar">Webinar</option>';
|
||||||
|
$html .= '</select>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
// Public checkbox
|
||||||
|
$html .= '<div class="form-group checkbox-group">';
|
||||||
|
$html .= '<label for="template-public">';
|
||||||
|
$html .= '<input type="checkbox" id="template-public" name="template_public" value="1">';
|
||||||
|
$html .= ' Make this template available to other trainers';
|
||||||
|
$html .= '</label>';
|
||||||
|
$html .= '<small class="description">If unchecked, only you will be able to use this template</small>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
$html .= '</div>';
|
||||||
|
$html .= '<div class="dialog-actions">';
|
||||||
|
$html .= '<button type="button" class="button button-primary" onclick="hvacSaveAsTemplate()">Save Template</button>';
|
||||||
|
$html .= '<button type="button" class="button" onclick="hvacCloseSaveDialog()">Cancel</button>';
|
||||||
|
$html .= '</div>';
|
||||||
|
$html .= '</div>';
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get timezone options for select field
|
* Get timezone options for select field
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ class HVAC_Shortcodes {
|
||||||
private function __construct() {
|
private function __construct() {
|
||||||
$this->define_shortcodes();
|
$this->define_shortcodes();
|
||||||
$this->register_shortcodes();
|
$this->register_shortcodes();
|
||||||
|
$this->register_ajax_handlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -860,4 +861,175 @@ class HVAC_Shortcodes {
|
||||||
$profile_manager = HVAC_Trainer_Profile_Manager::get_instance();
|
$profile_manager = HVAC_Trainer_Profile_Manager::get_instance();
|
||||||
return $profile_manager->render_profile_edit($atts);
|
return $profile_manager->render_profile_edit($atts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register AJAX handlers
|
||||||
|
*/
|
||||||
|
private function register_ajax_handlers() {
|
||||||
|
// Template preview AJAX handler
|
||||||
|
add_action('wp_ajax_hvac_get_template_preview', array($this, 'ajax_get_template_preview'));
|
||||||
|
add_action('wp_ajax_nopriv_hvac_get_template_preview', array($this, 'ajax_get_template_preview'));
|
||||||
|
|
||||||
|
// Template loading AJAX handler
|
||||||
|
add_action('wp_ajax_hvac_load_template_data', array($this, 'ajax_load_template_data'));
|
||||||
|
add_action('wp_ajax_nopriv_hvac_load_template_data', array($this, 'ajax_load_template_data'));
|
||||||
|
|
||||||
|
// Template saving AJAX handler
|
||||||
|
add_action('wp_ajax_hvac_save_template', array($this, 'ajax_save_template'));
|
||||||
|
add_action('wp_ajax_nopriv_hvac_save_template', array($this, 'ajax_save_template'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for getting template preview data
|
||||||
|
*/
|
||||||
|
public function ajax_get_template_preview() {
|
||||||
|
// Verify nonce
|
||||||
|
if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_event_form_nonce')) {
|
||||||
|
wp_send_json_error('Security check failed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$template_id = sanitize_text_field($_POST['template_id'] ?? '');
|
||||||
|
if (empty($template_id)) {
|
||||||
|
wp_send_json_error('No template ID provided');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get template manager instance
|
||||||
|
if (!class_exists('HVAC_Event_Template_Manager')) {
|
||||||
|
wp_send_json_error('Template manager not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$template_manager = HVAC_Event_Template_Manager::instance();
|
||||||
|
$template = $template_manager->get_template($template_id);
|
||||||
|
|
||||||
|
if (!$template) {
|
||||||
|
wp_send_json_error('Template not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return template data
|
||||||
|
wp_send_json_success($template);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for loading template data into form
|
||||||
|
*/
|
||||||
|
public function ajax_load_template_data() {
|
||||||
|
// Verify nonce
|
||||||
|
if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_event_form_nonce')) {
|
||||||
|
wp_send_json_error('Security check failed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$template_id = sanitize_text_field($_POST['template_id'] ?? '');
|
||||||
|
if (empty($template_id)) {
|
||||||
|
wp_send_json_error('No template ID provided');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get template manager instance
|
||||||
|
if (!class_exists('HVAC_Event_Template_Manager')) {
|
||||||
|
wp_send_json_error('Template manager not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$template_manager = HVAC_Event_Template_Manager::instance();
|
||||||
|
$template = $template_manager->get_template($template_id);
|
||||||
|
|
||||||
|
if (!$template || empty($template['field_data'])) {
|
||||||
|
wp_send_json_error('Template data not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return field data
|
||||||
|
wp_send_json_success($template['field_data']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for saving new template
|
||||||
|
*/
|
||||||
|
public function ajax_save_template() {
|
||||||
|
// Verify nonce
|
||||||
|
if (!wp_verify_nonce($_POST['nonce'] ?? '', 'hvac_event_form_nonce')) {
|
||||||
|
wp_send_json_error('Security check failed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user permissions
|
||||||
|
if (!is_user_logged_in()) {
|
||||||
|
wp_send_json_error('Authentication required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = wp_get_current_user();
|
||||||
|
if (!array_intersect(['hvac_trainer', 'hvac_master_trainer'], $user->roles)) {
|
||||||
|
wp_send_json_error('Insufficient permissions');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and sanitize input
|
||||||
|
$template_name = sanitize_text_field($_POST['template_name'] ?? '');
|
||||||
|
$template_description = sanitize_textarea_field($_POST['template_description'] ?? '');
|
||||||
|
$template_category = sanitize_text_field($_POST['template_category'] ?? '');
|
||||||
|
$is_public = (bool) ($_POST['is_public'] ?? false);
|
||||||
|
$field_data = $_POST['field_data'] ?? [];
|
||||||
|
|
||||||
|
if (empty($template_name)) {
|
||||||
|
wp_send_json_error('Template name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($template_category)) {
|
||||||
|
wp_send_json_error('Template category is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize field data
|
||||||
|
$sanitized_field_data = [];
|
||||||
|
foreach ($field_data as $key => $value) {
|
||||||
|
$sanitized_key = sanitize_key($key);
|
||||||
|
if (is_array($value)) {
|
||||||
|
$sanitized_field_data[$sanitized_key] = array_map('sanitize_text_field', $value);
|
||||||
|
} else {
|
||||||
|
$sanitized_field_data[$sanitized_key] = sanitize_text_field($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get template manager instance
|
||||||
|
if (!class_exists('HVAC_Event_Template_Manager')) {
|
||||||
|
wp_send_json_error('Template manager not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$template_manager = HVAC_Event_Template_Manager::instance();
|
||||||
|
|
||||||
|
// Prepare template data
|
||||||
|
$template_data = [
|
||||||
|
'name' => $template_name,
|
||||||
|
'description' => $template_description,
|
||||||
|
'category' => $template_category,
|
||||||
|
'is_public' => $is_public,
|
||||||
|
'field_data' => $sanitized_field_data,
|
||||||
|
'created_by' => $user->ID,
|
||||||
|
'meta_data' => [
|
||||||
|
'source' => 'event_form',
|
||||||
|
'created_at' => current_time('mysql'),
|
||||||
|
'user_ip' => $_SERVER['REMOTE_ADDR'] ?? '',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create the template
|
||||||
|
$result = $template_manager->create_template($template_data);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
wp_send_json_success([
|
||||||
|
'message' => 'Template saved successfully',
|
||||||
|
'template_id' => $result['template_id'] ?? null
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
wp_send_json_error($result['error'] ?? 'Failed to save template');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue