diff --git a/CLAUDE.md b/CLAUDE.md index a23fb118..b886406b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,5 +28,6 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - **Plugin Architecture Refactoring (2025-07-28)**: Implemented modular architecture with single-responsibility classes. Created HVAC_Shortcodes for centralized shortcode management, HVAC_Scripts_Styles for asset management, and HVAC_Route_Manager for URL routing. Eliminated duplicate functionality between HVAC_Plugin and HVAC_Community_Events. All components now use singleton pattern to prevent duplicate initialization. Fixed jQuery selector errors and duplicate content issues. See docs/ARCHITECTURE.md for details. - **Master Dashboard URL Fix (2025-07-29)**: Fixed critical issue where master dashboard was showing trainer dashboard content. Root cause: Both trainer and master dashboards had the same page slug "dashboard", causing WordPress to load the wrong page. Solution: Changed master dashboard URL from `/master-trainer/dashboard/` to `/master-trainer/master-dashboard/`, updated all code references, removed conflicting legacy redirects. Master dashboard now correctly displays master trainer content with aggregate statistics and trainer performance analytics. - **Event Manage Page CSS and Header Fix (2025-07-30)**: Resolved persistent CSS override and duplicate header issues on the trainer/event/manage/ page. Root causes: CSS specificity conflicts with theme styles, header being added via both template and tribe hook. Solution: Scoped all CSS rules to `.hvac-event-manage-wrapper`, moved navigation header directly into page template, disabled duplicate tribe hook, added theme override styles. Page now displays correctly with single header, proper 1200px max-width layout, 20px padding, and consistent styling matching other dashboard pages. +- **Major Plugin Update - Registration Refactor and New Trainer Pages (2025-07-30)**: Implemented comprehensive updates to HVAC plugin. Registration form refactored: moved Application Details to Personal Information, renamed Business Information to Training Organization Information with TEC integration, added required Organization Logo upload, added Headquarters location fields, created conditional Training Venue section. New trainer pages created: Training Venues system (/trainer/venue/list, /trainer/venue/manage) with full CRUD operations and TEC venue integration; Trainer Profile system (/trainer/profile, /trainer/profile/edit) with photo upload, certifications, stats tracking; Training Organizers system (/trainer/organizer/list, /trainer/organizer/manage) with logo upload and headquarters tracking. All systems use AJAX forms, real-time validation, responsive design, and proper WordPress/TEC integration. [... rest of the existing content remains unchanged ...] \ No newline at end of file diff --git a/assets/css/hvac-organizers.css b/assets/css/hvac-organizers.css new file mode 100644 index 00000000..126e11fb --- /dev/null +++ b/assets/css/hvac-organizers.css @@ -0,0 +1,434 @@ +/** + * HVAC Organizers Styles + * + * @package HVAC_Community_Events + * @version 2.0.0 + */ + +/* Page Layout */ +.hvac-organizers-list, +.hvac-organizer-manage { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +/* Page Header */ +.hvac-page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 2px solid #e0e0e0; +} + +.hvac-page-header h1 { + margin: 0; + color: #0274be; + font-size: 2rem; +} + +/* Breadcrumb */ +.hvac-breadcrumb { + margin-bottom: 1.5rem; + color: #666; + font-size: 0.9rem; +} + +.hvac-breadcrumb a { + color: #0274be; + text-decoration: none; +} + +.hvac-breadcrumb a:hover { + text-decoration: underline; +} + +/* Filters */ +.hvac-organizers-filters { + background-color: #f5f5f5; + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 2rem; +} + +.hvac-filter-form { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: flex-end; +} + +.hvac-filter-row { + display: flex; + gap: 1rem; + width: 100%; +} + +.hvac-filter-group { + flex: 1; + min-width: 200px; +} + +.hvac-filter-group input { + width: 100%; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; +} + +/* Table */ +.hvac-organizers-table-wrapper { + overflow-x: auto; + margin-bottom: 2rem; +} + +.hvac-organizers-table { + width: 100%; + border-collapse: collapse; + background-color: white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.hvac-organizers-table th { + background-color: #f8f9fa; + padding: 1rem; + text-align: left; + font-weight: 600; + color: #333; + border-bottom: 2px solid #e0e0e0; +} + +.hvac-organizers-table td { + padding: 1rem; + border-bottom: 1px solid #e0e0e0; + vertical-align: middle; +} + +.hvac-organizers-table tbody tr:hover { + background-color: #f8f9fa; +} + +.hvac-no-results { + text-align: center; + color: #666; + font-style: italic; + padding: 3rem !important; +} + +/* Logo Styles */ +.hvac-org-logo { + width: 60px; +} + +.hvac-org-logo img { + width: 50px; + height: 50px; + object-fit: cover; + border-radius: 4px; +} + +.hvac-logo-placeholder { + width: 50px; + height: 50px; + background-color: #0274be; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; + font-weight: 600; + border-radius: 4px; +} + +.hvac-logo-placeholder-large { + width: 200px; + height: 200px; + background-color: #f0f0f0; + color: #999; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + border-radius: 8px; + border: 2px dashed #ddd; +} + +/* Logo Upload */ +.hvac-org-logo-upload { + display: flex; + align-items: flex-start; + gap: 2rem; +} + +.hvac-current-logo img { + max-width: 200px; + max-height: 200px; + object-fit: contain; + border-radius: 8px; + border: 1px solid #e0e0e0; +} + +.hvac-logo-actions { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.hvac-help-text { + color: #666; + font-size: 0.875rem; + margin-top: 0.5rem; +} + +/* Buttons */ +.hvac-button { + display: inline-block; + padding: 0.75rem 1.5rem; + border: none; + border-radius: 4px; + font-size: 1rem; + font-weight: 600; + text-decoration: none; + cursor: pointer; + transition: all 0.3s ease; +} + +.hvac-button-primary { + background-color: #0274be; + color: white; +} + +.hvac-button-primary:hover { + background-color: #005fa3; +} + +.hvac-button-secondary { + background-color: #6c757d; + color: white; +} + +.hvac-button-secondary:hover { + background-color: #5a6268; +} + +.hvac-button-danger { + background-color: #dc3545; + color: white; +} + +.hvac-button-danger:hover { + background-color: #c82333; +} + +.hvac-button-danger-outline { + background-color: transparent; + color: #dc3545; + border: 1px solid #dc3545; +} + +.hvac-button-danger-outline:hover { + background-color: #dc3545; + color: white; +} + +.hvac-button-small { + padding: 0.5rem 1rem; + font-size: 0.875rem; +} + +.hvac-text-muted { + color: #6c757d; + font-style: italic; +} + +/* Pagination */ +.hvac-pagination { + display: flex; + justify-content: center; + gap: 0.5rem; + margin-top: 2rem; +} + +.hvac-pagination a, +.hvac-pagination span { + padding: 0.5rem 1rem; + border: 1px solid #ddd; + border-radius: 4px; + text-decoration: none; + color: #333; +} + +.hvac-pagination a:hover { + background-color: #f5f5f5; +} + +.hvac-pagination .current { + background-color: #0274be; + color: white; + border-color: #0274be; +} + +/* Form Styles */ +.hvac-form { + background-color: white; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.hvac-form-section { + margin-bottom: 2rem; + padding-bottom: 2rem; + border-bottom: 1px solid #e0e0e0; +} + +.hvac-form-section:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} + +.hvac-form-section h3 { + margin-bottom: 1.5rem; + color: #333; + font-size: 1.25rem; +} + +.hvac-form-row { + margin-bottom: 1.5rem; +} + +.hvac-form-row label { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; + color: #333; +} + +.hvac-form-row input, +.hvac-form-row select, +.hvac-form-row textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; +} + +.hvac-form-row input:focus, +.hvac-form-row select:focus, +.hvac-form-row textarea:focus { + outline: none; + border-color: #0274be; + box-shadow: 0 0 0 3px rgba(2, 116, 190, 0.1); +} + +.hvac-form-row-half { + display: flex; + gap: 1rem; +} + +.hvac-form-row-half > div { + flex: 1; +} + +.hvac-form-actions { + display: flex; + gap: 1rem; + margin-top: 2rem; + padding-top: 2rem; + border-top: 1px solid #e0e0e0; +} + +.hvac-form-actions .hvac-button-danger { + margin-left: auto; +} + +/* Messages */ +.hvac-message { + padding: 1rem 1.5rem; + border-radius: 4px; + margin-bottom: 1.5rem; + font-weight: 500; +} + +.hvac-message-success { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.hvac-message-error { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +/* Form Error Styles */ +.hvac-form-error { + border-color: #dc3545 !important; +} + +.hvac-error-message { + display: block; + color: #dc3545; + font-size: 0.875rem; + margin-top: 0.25rem; +} + +/* Responsive */ +@media (max-width: 768px) { + .hvac-organizers-list, + .hvac-organizer-manage { + padding: 1rem; + } + + .hvac-page-header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .hvac-filter-row { + flex-direction: column; + } + + .hvac-filter-group { + width: 100%; + } + + .hvac-form-row-half { + flex-direction: column; + } + + .hvac-form-actions { + flex-wrap: wrap; + } + + .hvac-form-actions .hvac-button-danger { + margin-left: 0; + width: 100%; + } + + .hvac-organizers-table { + font-size: 0.875rem; + } + + .hvac-organizers-table th, + .hvac-organizers-table td { + padding: 0.5rem; + } + + .hvac-org-logo-upload { + flex-direction: column; + align-items: center; + } + + .hvac-logo-actions { + width: 100%; + align-items: stretch; + } +} \ No newline at end of file diff --git a/assets/css/hvac-registration.css b/assets/css/hvac-registration.css new file mode 100644 index 00000000..c536a8a4 --- /dev/null +++ b/assets/css/hvac-registration.css @@ -0,0 +1,1003 @@ +/* Reduced Motion Support Added - 2025-07-23 */ +/* Vendor Prefixes Added - 2025-07-23 */ +/** + * HVAC Trainer Registration Form Enhanced Styles + * + * @version 2.0.0 + */ + +/* Error Styles */ +.hvac-form-errors { + background-color: #fee; + border: 1px solid #fcc; + border-radius: 4px; + padding: 1rem; + margin-bottom: 2rem; +} + +.hvac-form-errors h3 { + color: #d8000c; + margin-top: 0; + margin-bottom: 0.5rem; + font-size: 1.1rem; +} + +.hvac-form-errors ul { + margin: 0; + padding-left: 1.5rem; +} + +.hvac-form-errors li { + color: #d8000c; + margin-bottom: 0.25rem; +} + +.error-message { + color: #d8000c; + font-size: 0.875rem; + margin-top: 0.25rem; + margin-bottom: 0; +} + +input.error, +select.error, +textarea.error { + border-color: #fcc; +} + +/* Main Container */ +.hvac-registration-form-wrapper { + + max-width: 1200px; + + margin: 0 auto; + + padding: 2rem; /* IE fallback */ + + padding: var(--hvac-spacing-xl) var(--hvac-spacing-md); + + background-color: #f9fafb; +} + +/* Form Card */ +.hvac-registration-form { + + max-width: 800px; + + margin: 0 auto; + + padding: 2rem; /* IE fallback */ + + padding: var(--hvac-spacing-xl); + + background-color: white; + + -webkit-webkit-webkit-border-radius: 4px; + + border-radius: 4px; + + border-radius: 4px; /* IE fallback */ + + -webkit-border-radius: var(--hvac-border-radius); + -webkit-box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* IE fallback */ + + -webkit-box-shadow: var(--hvac-shadow-lg); + + box-shadow: var(--hvac-shadow-lg); + + border: 1px solid #e0e0e0; /* IE fallback */ + + border: 1px solid var(--hvac-border); +} + +/* Form Header */ +.hvac-registration-form-header { + + text-align: center; + + margin-bottom: 2rem; /* IE fallback */ + + margin-bottom: var(--hvac-spacing-xl); + + padding-bottom: 1rem; /* IE fallback */ + + padding-bottom: var(--hvac-spacing-md); + + border-bottom: 1px solid #f0f0f0; /* IE fallback */ + + border-bottom: 1px solid var(--hvac-border-light); +} + +.hvac-registration-form h2 { + + color: #0274be; /* IE fallback */ + + color: var(--hvac-primary); + + font-size: 1.8rem; + + margin-bottom: 0.5rem; /* IE fallback */ + + margin-bottom: var(--hvac-spacing-sm); + + font-weight: 700; +} + +.hvac-registration-form-header p { + + color: #757575; /* IE fallback */ + + color: var(--hvac-text-light); + + font-size: 1rem; + + max-width: 600px; + + margin: 0 auto; +} + +/* Form Sections */ +.hvac-registration-form .form-section { + + margin-bottom: 2rem; /* IE fallback */ + + margin-bottom: var(--hvac-spacing-xl); + + padding-bottom: 1.5rem; /* IE fallback */ + + padding-bottom: var(--hvac-spacing-lg); + + border-bottom: 1px solid #f0f0f0; /* IE fallback */ + + border-bottom: 1px solid var(--hvac-border-light); +} + +.hvac-registration-form .form-section:last-child { + margin-bottom: 0; + + padding-bottom: 0; + + border-bottom: none; +} + +.hvac-registration-form .form-section h3 { + + color: #3a3f44; /* IE fallback */ + + color: var(--hvac-secondary-dark); + + margin-bottom: 1rem; /* IE fallback */ + + margin-bottom: var(--hvac-spacing-md); + + font-size: 1.3rem; + + font-weight: 600; + + padding-bottom: 0.5rem; /* IE fallback */ + + padding-bottom: var(--hvac-spacing-sm); + + border-bottom: 1px dashed #f0f0f0; /* IE fallback */ + + border-bottom: 1px dashed var(--hvac-border-light); +} + +/* Form Grid Layout */ +.hvac-registration-form .form-grid { + + display: grid; + + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + + gap: 1rem; /* IE fallback */ + + gap: var(--hvac-spacing-md); +} + +/* Form Rows */ +.hvac-registration-form .form-row { + + margin-bottom: 1rem; /* IE fallback */ + + margin-bottom: var(--hvac-spacing-md); +} + +/* Two-column layout */ +.form-row-half { + display: flex; + gap: 1rem; +} + +.form-row-half > div { + flex: 1; +} + +/* Three-column layout */ +.form-row-thirds { + display: flex; + gap: 1rem; +} + +.form-row-thirds > div { + flex: 1; +} + +/* Form Fields */ +.hvac-registration-form label { + + display: block; + + margin-bottom: 0.5rem; /* IE fallback */ + + margin-bottom: var(--hvac-spacing-sm); + + font-weight: 600; + + color: #333333; /* IE fallback */ + + color: var(--hvac-text); + + font-size: 0.95rem; +} + +.hvac-registration-form label .required { + + color: #d63638; /* IE fallback */ + + color: var(--hvac-error); + + margin-left: 0.25rem; /* IE fallback */ + + margin-left: var(--hvac-spacing-xs); +} + +.hvac-registration-form input[type="text"], +.hvac-registration-form input[type="email"], +.hvac-registration-form input[type="password"], +.hvac-registration-form input[type="url"], +.hvac-registration-form textarea, +.hvac-registration-form select { + + width: 100%; + + padding: 0.85rem; + + border: 1px solid #e0e0e0; /* IE fallback */ + + border: 1px solid var(--hvac-border); + + border-radius: 4px; /* IE fallback */ + + border-radius: var(--hvac-border-radius); + + font-size: 1rem; + + -webkit-transition: border-color 0.2s, box-shadow 0.2s; + + background-color: #f9fafb; +} + +.hvac-registration-form input[type="text"]:focus, +.hvac-registration-form input[type="email"]:focus, +.hvac-registration-form input[type="password"]:focus, +.hvac-registration-form input[type="url"]:focus, +.hvac-registration-form textarea:focus, +.hvac-registration-form select:focus { + border-color: #0274be; /* IE fallback */ + + border-color: var(--hvac-primary); + + outline: none; + + -webkit-box-shadow: 0 0 0 3px #e6f3fb;/* IE fallback */ + + -webkit-box-shadow: 0 0 0 3px var(--hvac-primary-light); + + background-color: white; +} + +.hvac-registration-form textarea { + + min-height: 120px; + + resize: vertical; +} + +.hvac-registration-form select { + + -webkit-appearance: none; + + -moz-appearance: none; + + background-image: url("data: image/svg+xml; + charset=utf-8,%3Csvg xmlns='; + + http: //www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); + + background-repeat: no-repeat; + + background-position: right 0.75rem center; + + background-size: 16px 12px; + + padding-right: 2.5rem; +} + +/* Field Helper Text */ +.hvac-registration-form .field-help { + + margin-top: 0.25rem; /* IE fallback */ + + margin-top: var(--hvac-spacing-xs); + + font-size: 0.85rem; + + color: #757575; /* IE fallback */ + + color: var(--hvac-text-light); +} + +/* Submit Button Section */ +.hvac-registration-form .form-submit { + + margin-top: 2rem; /* IE fallback */ + + margin-top: var(--hvac-spacing-xl); + + text-align: center; +} + +.hvac-registration-form input[type="submit"] { + + display: inline-block; + + padding: 0.85rem 2.5rem; + + background-color: #0274be; /* IE fallback */ + + background-color: var(--hvac-primary); + + color: white; + + border: none; + + border-radius: 4px; /* IE fallback */ + + border-radius: var(--hvac-border-radius); + + font-size: 1.1rem; + + font-weight: 600; + + cursor: pointer; + + text-align: center; + + -webkit-transition: background-color 0.2s, transform 0.1s; + + text-transform: uppercase; + + letter-spacing: 0.5px; + + -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* IE fallback */ + + -webkit-box-shadow: var(--hvac-shadow); +} + +.hvac-registration-form input[type="submit"]:hover { + + background-color: #005fa3; /* IE fallback */ + + background-color: var(--hvac-primary-dark); + + -webkit-transform: translateY(-2px); + + -ms-transform: translateY(-2px); + + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* IE fallback */ + + box-shadow: var(--hvac-shadow-lg); +} + +.hvac-registration-form input[type="submit"]:active { + + -webkit-transform: translateY(0); + + -ms-transform: translateY(0); +} + +/* Checkbox & Radio Styles */ +.hvac-registration-form .checkbox-group { + + display: -webkit-box; + + display: -ms-flexbox; + + display: flex; + + -webkit-box-orient: vertical; + + -webkit-box-direction: normal; + + -ms-flex-direction: column; + + gap: 0.5rem; /* IE fallback */ + + gap: var(--hvac-spacing-sm); + + margin-top: 0.5rem; /* IE fallback */ + + margin-top: var(--hvac-spacing-sm); +} + +.hvac-registration-form .checkbox-group label { + + display: -webkit-box; + + display: -ms-flexbox; + + display: flex; + + -webkit-box-align: center; + + -ms-flex-align: center; + + align-items: center; + + gap: 0.5rem; /* IE fallback */ + + gap: var(--hvac-spacing-sm); + + font-weight: normal; + + cursor: pointer; + + margin-bottom: 0; +} + +.hvac-registration-form .checkbox-group input[type="checkbox"], +.hvac-registration-form .checkbox-group input[type="radio"] { + + width: 18px; + + height: 18px; + + cursor: pointer; +} + +/* Error Messages */ +.hvac-registration-form .hvac-errors { + + background-color: #ffebe9; /* IE fallback */ + + background-color: var(--hvac-error-light); + + border: 1px solid #d63638; /* IE fallback */ + + border: 1px solid var(--hvac-error); + + border-radius: 4px; /* IE fallback */ + + border-radius: var(--hvac-border-radius); + + padding: 1rem; /* IE fallback */ + + padding: var(--hvac-spacing-md); + + margin-bottom: 1.5rem; /* IE fallback */ + + margin-bottom: var(--hvac-spacing-lg); + + color: #d63638; /* IE fallback */ + + color: var(--hvac-error); +} + +.hvac-registration-form .hvac-errors .error { + + margin: 0.25rem; /* IE fallback */ + + margin: var(--hvac-spacing-xs) 0; +} + +.hvac-registration-form .form-row.has-error input, +.hvac-registration-form .form-row.has-error select, +.hvac-registration-form .form-row.has-error textarea { + + border-color: #d63638; /* IE fallback */ + + border-color: var(--hvac-error); +} + +.hvac-registration-form .form-row.has-error .field-error { + + color: #d63638; /* IE fallback */ + + color: var(--hvac-error); + + font-size: 0.85rem; + + margin-top: 0.25rem; /* IE fallback */ + + margin-top: var(--hvac-spacing-xs); +} + +/* Success Message */ +.hvac-registration-form .hvac-success { + + background-color: #e8f5e9; /* IE fallback */ + + background-color: var(--hvac-success-light); + + border: 1px solid #4caf50; /* IE fallback */ + + border: 1px solid var(--hvac-success); + + border-radius: 4px; /* IE fallback */ + + border-radius: var(--hvac-border-radius); + + padding: 1rem; /* IE fallback */ + + padding: var(--hvac-spacing-md); + + margin-bottom: 1.5rem; /* IE fallback */ + + margin-bottom: var(--hvac-spacing-lg); + + color: #4caf50; /* IE fallback */ + + color: var(--hvac-success); +} + +/* Login Link */ +.hvac-login-link { + + margin-top: 1.5rem; /* IE fallback */ + + margin-top: var(--hvac-spacing-lg); + + text-align: center; + + padding-top: 1rem; /* IE fallback */ + + padding-top: var(--hvac-spacing-md); + + border-top: 1px solid #f0f0f0; /* IE fallback */ + + border-top: 1px solid var(--hvac-border-light); + + font-size: 0.95rem; +} + +.hvac-login-link a { + + color: #0274be; /* IE fallback */ + + color: var(--hvac-primary); + + font-weight: 600; + + text-decoration: none; +} + +.hvac-login-link a:hover { + text-decoration: underline; +} + +/* Progress Indicator for Multi-step Forms */ +.hvac-registration-progress { + + margin-bottom: 2rem; /* IE fallback */ + + margin-bottom: var(--hvac-spacing-xl); + + padding: 1rem; /* IE fallback */ + + padding: var(--hvac-spacing-md); + + background-color: #f0f0f1; /* IE fallback */ + + background-color: var(--hvac-secondary-light); + + border-radius: 4px; /* IE fallback */ + + border-radius: var(--hvac-border-radius); + + display: -webkit-box; + + display: -ms-flexbox; + + display: flex; + + -webkit-box-pack: justify; + + -ms-flex-pack: justify; + + justify-content: space-between; + + position: relative; +} + +.hvac-registration-progress::before { + content: ''; + + position: absolute; + + top: 50%; + + left: 1rem; /* IE fallback */ + + left: var(--hvac-spacing-md); + + right: 1rem; /* IE fallback */ + + right: var(--hvac-spacing-md); + + height: 2px; + + background-color: #e0e0e0; /* IE fallback */ + + background-color: var(--hvac-border); + + -webkit-transform: translateY(-50%); + + -ms-transform: translateY(-50%); + + z-index: 1; +} + +.hvac-registration-step { + + width: 30px; + + height: 30px; + + -webkit-border-radius: 50%; + + background-color: white; + + border: 2px solid #e0e0e0; /* IE fallback */ + + border: 2px solid var(--hvac-border); + + display: -webkit-box; + + display: -ms-flexbox; + + display: flex; + + -webkit-box-align: center; + + -ms-flex-align: center; + + align-items: center; + + -webkit-box-pack: center; + + -ms-flex-pack: center; + + justify-content: center; + + font-weight: 600; + + position: relative; + + z-index: 2; +} + +.hvac-registration-step.active { + + background-color: #0274be; /* IE fallback */ + + background-color: var(--hvac-primary); + + border-color: #0274be; /* IE fallback */ + + border-color: var(--hvac-primary); + + color: white; +} + +.hvac-registration-step.completed { + + background-color: #4caf50; /* IE fallback */ + + background-color: var(--hvac-success); + + border-color: #4caf50; /* IE fallback */ + + border-color: var(--hvac-success); + + color: white; +} + +/* Responsive Adjustments */ + +/* Reduced Motion Support Added - WCAG 2.1 Accessibility */ +/* Respects user preference for reduced motion to prevent vestibular disorders */ + +@media (prefers-reduced-motion: reduce) { + /* Disable all animations and transitions globally */ + *, *::before, *::after { + animation-duration: 0.001ms !important; + animation-delay: 0s !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + transition-delay: 0s !important; + scroll-behavior: auto !important; + } + + /* Remove specific transform animations */ + .hvac-animate-fade-in, + .hvac-animate-scale-up, + .hvac-animate-pulse, + .hvac-animate-slide-in-right, + .hvac-animate-slide-in-left, + .hvac-animate-slide-in-bottom { + animation: none !important; + opacity: 1 !important; + transform: none !important; + } + + /* Disable hover transformations */ + .hvac-card:hover, + .hvac-stat-card:hover, + .hvac-event-stat-card:hover, + .hvac-button:hover, + .hvac-email-submit:hover { + transform: none !important; + animation: none !important; + } + + /* Keep essential visual feedback but remove motion */ + .hvac-card:hover, + .hvac-stat-card:hover, + .hvac-event-stat-card:hover { + border-color: var(--hvac-primary, #0274be) !important; + box-shadow: 0 0 0 2px rgba(2, 116, 190, 0.2) !important; + } + + /* Disable loading spinner animation but keep visibility */ + .hvac-loading::after { + animation: none !important; + border-radius: 50% !important; + border: 2px solid rgba(0, 0, 0, 0.2) !important; + border-top-color: #333 !important; + } + + /* Disable focus pulse animation */ + .hvac-button:focus, + .hvac-email-submit:focus, + .hvac-content button[type="submit"]:focus { + animation: none !important; + } + + /* Ensure smooth scrolling is disabled */ + html { + scroll-behavior: auto !important; + } + + /* Disable CSS Grid/Flexbox animations if any */ + .hvac-dashboard-stats .hvac-stat-card:nth-child(n), + .hvac-event-summary-stats .hvac-event-stat-card:nth-child(n) { + animation: none !important; + opacity: 1 !important; + } +} + +/* Provide alternative visual feedback for reduced motion users */ +@media (prefers-reduced-motion: reduce) { + /* Enhanced border feedback instead of transform */ + .hvac-content button:hover, + .hvac-content input[type="submit"]:hover, + .hvac-content a:hover { + outline: 2px solid var(--hvac-primary, #0274be) !important; + + outline-offset: 2px !important; + } + + /* Enhanced color changes for interactive elements */ + .hvac-attendee-item:hover { + background-color: var(--hvac-primary-light, #e6f3fb) !important; + + border-left: 4px solid var(--hvac-primary, #0274be) !important; + } + + /* Static loading indicator */ + .hvac-loading { + + opacity: 0.7 !important; + } + + .hvac-loading::after { + content: "Loading..." !important; + display: inline-block !important; + font-size: 12px !important; + color: #666 !important; + border: none !important; + background: none !important; + border-radius: 0 !important; + width: auto !important; + height: auto !important; + position: static !important; + margin-left: 8px !important; + } +} + +@media (max-width: 768px) { + .hvac-registration-form { + padding: 1.5rem; + /* IE fallback */ + padding: var(--hvac-spacing-lg); + } + + .hvac-registration-form .form-grid { + + grid-template-columns: 1fr; + } + + .hvac-registration-form h2 { + + font-size: 1.5rem; + } + + .hvac-registration-progress { + + padding: 0.5rem; /* IE fallback */ + padding: var(--hvac-spacing-sm); + } +} + +@media (max-width: 480px) { + .hvac-registration-form { + padding: 1rem; + /* IE fallback */ + padding: var(--hvac-spacing-md); + } + + .hvac-registration-form h2 { + + font-size: 1.3rem; + } + + .hvac-registration-form .form-submit input[type="submit"] { + width: 100%; + } +} + +/* Focus Management Styles - WCAG 2.1 Compliance */ +/* Added for keyboard accessibility and screen reader support */ + +/* Button Focus Styles */ +.hvac-button:focus, +.hvac-content .button:focus, +.hvac-content button:focus, +.hvac-content input[type="submit"]:focus, +.hvac-email-submit:focus, +.hvac-filter-submit:focus, +.hvac-certificate-actions button:focus, +.hvac-certificate-actions a:focus { + outline: 2px solid #005fcc; + + outline-offset: 2px; + + -webkit-box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.2); + + box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.2); + + border-radius: 4px; +} + +/* Input Focus Styles */ +.hvac-form-input:focus, +.hvac-content input[type="text"]:focus, +.hvac-content input[type="email"]:focus, +.hvac-content input[type="password"]:focus, +.hvac-content input[type="url"]:focus, +.hvac-content textarea:focus, +.hvac-content select:focus, +.hvac-email-form-row input:focus, +.hvac-email-form-row textarea:focus, +.hvac-filter-group input:focus, +.hvac-filter-group select:focus { + outline: 2px solid #005fcc; + + outline-offset: 2px; + + border-color: #005fcc; + + box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.2); +} + +/* Link Focus Styles */ +.hvac-content a:focus, +.hvac-event-link:focus, +.hvac-certificate-link:focus, +.hvac-attendee-profile-icon:focus, +.hvac-dashboard-nav a:focus, +.hvac-email-navigation a:focus { + outline: 2px solid #005fcc; + outline-offset: 2px; + text-decoration: underline; + background-color: rgba(0, 95, 204, 0.1); + -webkit-border-radius: 2px; +} + +/* Interactive Element Focus Styles */ +.hvac-attendee-checkbox:focus, +.hvac-select-all-container input[type="checkbox"]:focus, +.hvac-modal-close:focus, +.hvac-certificate-table tr:focus { + outline: 2px solid #005fcc; + outline-offset: 2px; + box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.2); +} + +/* High Contrast Mode Support */ +@media (prefers-contrast: high) { + .hvac-content *:focus { + outline: 3px solid #000000; + outline-offset: 2px; + background-color: #ffff00; + color: #000000; + } +} + +/* Focus-visible polyfill support */ + +/* Reset focus for mouse users while preserving keyboard accessibility */ +.js-focus-visible:focus:not(.focus-visible) { + outline: none; + + -webkit-box-shadow: none; +} + +/* Ensure focus is visible for keyboard users */ +.js-focus-visible .focus-visible { + + outline: 2px solid #005fcc; + + outline-offset: 2px; +} + +/* CSS Grid Fallbacks for IE */ +.hvac-stats-row, +.hvac-dashboard-stats, +.hvac-certificate-stats { + + display: -ms-grid; + + -ms-grid-columns: repeat(auto-fit, minmax(200px, 1fr)); +} + +/* Progressive enhancement for modern browsers */ +@supports (display: grid) { + .hvac-stats-row, + .hvac-dashboard-stats, + .hvac-certificate-stats { + display: grid; + } +} + +/* Feature Detection Support */ +@supports not (display: flex) { + .hvac-content [class*="flex"] { + display: table-cell; + vertical-align: middle; + } +} + +@supports not (display: grid) { + .hvac-content [class*="grid"] { + display: block; + + overflow: hidden; + } + + .hvac-content [class*="grid"] > * { + float: left; + width: 50%; + } +} \ No newline at end of file diff --git a/assets/css/hvac-trainer-profile.css b/assets/css/hvac-trainer-profile.css new file mode 100644 index 00000000..18773f20 --- /dev/null +++ b/assets/css/hvac-trainer-profile.css @@ -0,0 +1,419 @@ +/** + * HVAC Trainer Profile Styles + * + * @package HVAC_Community_Events + * @version 2.0.0 + */ + +/* Page Layout */ +.hvac-trainer-profile-view, +.hvac-trainer-profile-edit { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +/* Page Header */ +.hvac-page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 2px solid #e0e0e0; +} + +.hvac-page-header h1 { + margin: 0; + color: #0274be; + font-size: 2rem; +} + +/* Breadcrumb */ +.hvac-breadcrumb { + margin-bottom: 1.5rem; + color: #666; + font-size: 0.9rem; +} + +.hvac-breadcrumb a { + color: #0274be; + text-decoration: none; +} + +.hvac-breadcrumb a:hover { + text-decoration: underline; +} + +/* Profile Content Layout */ +.hvac-profile-content { + display: flex; + gap: 2rem; +} + +/* Profile Sidebar */ +.hvac-profile-sidebar { + flex: 0 0 300px; +} + +/* Profile Photo */ +.hvac-profile-photo { + margin-bottom: 2rem; + text-align: center; +} + +.hvac-profile-photo img { + width: 200px; + height: 200px; + border-radius: 50%; + object-fit: cover; + border: 5px solid #f0f0f0; +} + +.hvac-profile-photo-placeholder { + width: 200px; + height: 200px; + border-radius: 50%; + background-color: #0274be; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 3rem; + font-weight: 600; + margin: 0 auto; +} + +/* Profile Stats */ +.hvac-profile-stats { + background-color: #f8f9fa; + padding: 1.5rem; + border-radius: 8px; +} + +.hvac-stat-item { + text-align: center; + margin-bottom: 1.5rem; +} + +.hvac-stat-item:last-child { + margin-bottom: 0; +} + +.hvac-stat-value { + display: block; + font-size: 2rem; + font-weight: 700; + color: #0274be; + margin-bottom: 0.5rem; +} + +.hvac-stat-label { + display: block; + font-size: 0.875rem; + color: #666; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* Profile Main Content */ +.hvac-profile-main { + flex: 1; +} + +.hvac-profile-section { + background-color: white; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin-bottom: 2rem; +} + +.hvac-profile-section h2 { + margin-top: 0; + margin-bottom: 1.5rem; + color: #333; + font-size: 1.5rem; + padding-bottom: 0.75rem; + border-bottom: 2px solid #e0e0e0; +} + +/* Profile Details */ +.hvac-profile-details { + display: grid; + gap: 1rem; +} + +.hvac-detail-row { + display: grid; + grid-template-columns: 150px 1fr; + align-items: center; +} + +.hvac-detail-label { + font-weight: 600; + color: #666; +} + +.hvac-detail-value { + color: #333; +} + +.hvac-detail-value a { + color: #0274be; + text-decoration: none; +} + +.hvac-detail-value a:hover { + text-decoration: underline; +} + +/* Profile Bio */ +.hvac-profile-bio { + color: #333; + line-height: 1.6; +} + +/* Certifications List */ +.hvac-certifications-list { + display: grid; + gap: 0.75rem; +} + +.hvac-certification-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem; + background-color: #f8f9fa; + border-radius: 4px; +} + +.hvac-certification-item .dashicons { + color: #0274be; + width: 20px; + height: 20px; +} + +/* Edit Form Styles */ +.hvac-form { + background-color: white; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.hvac-form-section { + margin-bottom: 2rem; + padding-bottom: 2rem; + border-bottom: 1px solid #e0e0e0; +} + +.hvac-form-section:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} + +.hvac-form-section h3 { + margin-bottom: 1.5rem; + color: #333; + font-size: 1.25rem; +} + +.hvac-form-row { + margin-bottom: 1.5rem; +} + +.hvac-form-row label { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; + color: #333; +} + +.hvac-form-row input, +.hvac-form-row select, +.hvac-form-row textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; +} + +.hvac-form-row input:focus, +.hvac-form-row select:focus, +.hvac-form-row textarea:focus { + outline: none; + border-color: #0274be; + box-shadow: 0 0 0 3px rgba(2, 116, 190, 0.1); +} + +.hvac-form-row-half { + display: flex; + gap: 1rem; +} + +.hvac-form-row-half > div { + flex: 1; +} + +/* Profile Photo Upload */ +.hvac-profile-photo-upload { + display: flex; + align-items: center; + gap: 2rem; +} + +.hvac-current-photo img { + width: 100px; + height: 100px; + border-radius: 50%; + object-fit: cover; +} + +.hvac-photo-placeholder { + width: 100px; + height: 100px; + border-radius: 50%; + background-color: #f0f0f0; + display: flex; + align-items: center; + justify-content: center; + color: #999; + font-size: 0.875rem; +} + +.hvac-photo-actions { + display: flex; + gap: 1rem; +} + +/* Buttons */ +.hvac-button { + display: inline-block; + padding: 0.75rem 1.5rem; + border: none; + border-radius: 4px; + font-size: 1rem; + font-weight: 600; + text-decoration: none; + cursor: pointer; + transition: all 0.3s ease; +} + +.hvac-button-primary { + background-color: #0274be; + color: white; +} + +.hvac-button-primary:hover { + background-color: #005fa3; +} + +.hvac-button-secondary { + background-color: #6c757d; + color: white; +} + +.hvac-button-secondary:hover { + background-color: #5a6268; +} + +.hvac-button-danger-outline { + background-color: transparent; + color: #dc3545; + border: 1px solid #dc3545; +} + +.hvac-button-danger-outline:hover { + background-color: #dc3545; + color: white; +} + +/* Form Actions */ +.hvac-form-actions { + display: flex; + gap: 1rem; + margin-top: 2rem; + padding-top: 2rem; + border-top: 1px solid #e0e0e0; +} + +/* Messages */ +.hvac-message { + padding: 1rem 1.5rem; + border-radius: 4px; + margin-bottom: 1.5rem; + font-weight: 500; +} + +.hvac-message-success { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.hvac-message-error { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +/* Form Errors */ +.hvac-form-error { + border-color: #dc3545 !important; +} + +.hvac-error-message { + display: block; + color: #dc3545; + font-size: 0.875rem; + margin-top: 0.25rem; +} + +/* Responsive */ +@media (max-width: 768px) { + .hvac-trainer-profile-view, + .hvac-trainer-profile-edit { + padding: 1rem; + } + + .hvac-page-header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .hvac-profile-content { + flex-direction: column; + } + + .hvac-profile-sidebar { + flex: none; + width: 100%; + } + + .hvac-detail-row { + grid-template-columns: 1fr; + gap: 0.25rem; + } + + .hvac-detail-label { + font-size: 0.875rem; + } + + .hvac-form-row-half { + flex-direction: column; + } + + .hvac-profile-photo-upload { + flex-direction: column; + align-items: flex-start; + } + + .hvac-photo-actions { + flex-wrap: wrap; + } +} \ No newline at end of file diff --git a/assets/css/hvac-venues.css b/assets/css/hvac-venues.css new file mode 100644 index 00000000..367a9c6f --- /dev/null +++ b/assets/css/hvac-venues.css @@ -0,0 +1,357 @@ +/** + * HVAC Venues Styles + * + * @package HVAC_Community_Events + * @version 2.0.0 + */ + +/* Page Layout */ +.hvac-venues-list, +.hvac-venue-manage { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; +} + +/* Page Header */ +.hvac-page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 2px solid #e0e0e0; +} + +.hvac-page-header h1 { + margin: 0; + color: #0274be; + font-size: 2rem; +} + +/* Breadcrumb */ +.hvac-breadcrumb { + margin-bottom: 1.5rem; + color: #666; + font-size: 0.9rem; +} + +.hvac-breadcrumb a { + color: #0274be; + text-decoration: none; +} + +.hvac-breadcrumb a:hover { + text-decoration: underline; +} + +/* Filters */ +.hvac-venues-filters { + background-color: #f5f5f5; + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 2rem; +} + +.hvac-filter-form { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: flex-end; +} + +.hvac-filter-group { + flex: 1; + min-width: 200px; +} + +.hvac-filter-group input, +.hvac-filter-group select { + width: 100%; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; +} + +/* Table */ +.hvac-venues-table-wrapper { + overflow-x: auto; + margin-bottom: 2rem; +} + +.hvac-venues-table { + width: 100%; + border-collapse: collapse; + background-color: white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.hvac-venues-table th { + background-color: #f8f9fa; + padding: 1rem; + text-align: left; + font-weight: 600; + color: #333; + border-bottom: 2px solid #e0e0e0; +} + +.hvac-venues-table td { + padding: 1rem; + border-bottom: 1px solid #e0e0e0; +} + +.hvac-venues-table tbody tr:hover { + background-color: #f8f9fa; +} + +.hvac-no-results { + text-align: center; + color: #666; + font-style: italic; + padding: 3rem !important; +} + +/* Badges */ +.hvac-badge { + display: inline-block; + padding: 0.25rem 0.5rem; + border-radius: 3px; + font-size: 0.75rem; + font-weight: 600; + margin-left: 0.5rem; +} + +.hvac-badge-owner { + background-color: #e3f2fd; + color: #1976d2; +} + +/* Buttons */ +.hvac-button { + display: inline-block; + padding: 0.75rem 1.5rem; + border: none; + border-radius: 4px; + font-size: 1rem; + font-weight: 600; + text-decoration: none; + cursor: pointer; + transition: all 0.3s ease; +} + +.hvac-button-primary { + background-color: #0274be; + color: white; +} + +.hvac-button-primary:hover { + background-color: #005fa3; +} + +.hvac-button-secondary { + background-color: #6c757d; + color: white; +} + +.hvac-button-secondary:hover { + background-color: #5a6268; +} + +.hvac-button-danger { + background-color: #dc3545; + color: white; +} + +.hvac-button-danger:hover { + background-color: #c82333; +} + +.hvac-button-small { + padding: 0.5rem 1rem; + font-size: 0.875rem; +} + +.hvac-text-muted { + color: #6c757d; + font-style: italic; +} + +/* Pagination */ +.hvac-pagination { + display: flex; + justify-content: center; + gap: 0.5rem; + margin-top: 2rem; +} + +.hvac-pagination a, +.hvac-pagination span { + padding: 0.5rem 1rem; + border: 1px solid #ddd; + border-radius: 4px; + text-decoration: none; + color: #333; +} + +.hvac-pagination a:hover { + background-color: #f5f5f5; +} + +.hvac-pagination .current { + background-color: #0274be; + color: white; + border-color: #0274be; +} + +/* Form Styles */ +.hvac-form { + background-color: white; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.hvac-form-section { + margin-bottom: 2rem; + padding-bottom: 2rem; + border-bottom: 1px solid #e0e0e0; +} + +.hvac-form-section:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} + +.hvac-form-section h3 { + margin-bottom: 1.5rem; + color: #333; + font-size: 1.25rem; +} + +.hvac-form-row { + margin-bottom: 1.5rem; +} + +.hvac-form-row label { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; + color: #333; +} + +.hvac-form-row input, +.hvac-form-row select, +.hvac-form-row textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; +} + +.hvac-form-row input:focus, +.hvac-form-row select:focus, +.hvac-form-row textarea:focus { + outline: none; + border-color: #0274be; + box-shadow: 0 0 0 3px rgba(2, 116, 190, 0.1); +} + +.hvac-form-row-half { + display: flex; + gap: 1rem; +} + +.hvac-form-row-half > div { + flex: 1; +} + +.hvac-form-actions { + display: flex; + gap: 1rem; + margin-top: 2rem; + padding-top: 2rem; + border-top: 1px solid #e0e0e0; +} + +.hvac-form-actions .hvac-button-danger { + margin-left: auto; +} + +/* Responsive */ +@media (max-width: 768px) { + .hvac-venues-list, + .hvac-venue-manage { + padding: 1rem; + } + + .hvac-page-header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .hvac-filter-form { + flex-direction: column; + } + + .hvac-filter-group { + width: 100%; + } + + .hvac-form-row-half { + flex-direction: column; + } + + .hvac-form-actions { + flex-wrap: wrap; + } + + .hvac-form-actions .hvac-button-danger { + margin-left: 0; + width: 100%; + } + + .hvac-venues-table { + font-size: 0.875rem; + } + + .hvac-venues-table th, + .hvac-venues-table td { + padding: 0.5rem; + } +} + +/* Messages */ +.hvac-message { + padding: 1rem 1.5rem; + border-radius: 4px; + margin-bottom: 1.5rem; + font-weight: 500; +} + +.hvac-message-success { + background-color: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.hvac-message-error { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +/* Form Error Styles */ +.hvac-form-error { + border-color: #dc3545 !important; +} + +.hvac-error-message { + display: block; + color: #dc3545; + font-size: 0.875rem; + margin-top: 0.25rem; +} \ No newline at end of file diff --git a/assets/js/hvac-organizers.js b/assets/js/hvac-organizers.js new file mode 100644 index 00000000..85236de0 --- /dev/null +++ b/assets/js/hvac-organizers.js @@ -0,0 +1,364 @@ +/** + * HVAC Organizers JavaScript + * + * @package HVAC_Community_Events + * @version 2.0.0 + */ + +jQuery(document).ready(function($) { + // Cache DOM elements + const $organizerForm = $('#hvac-organizer-form'); + const $uploadButton = $('#hvac-upload-logo'); + const $removeButton = $('#hvac-remove-logo'); + const $logoIdField = $('#org_logo_id'); + const $currentLogo = $('.hvac-current-logo'); + const $deleteButton = $('#hvac-delete-organizer'); + + // Form validation + function validateOrganizerForm() { + let isValid = true; + const errors = []; + + // Clear previous errors + $('.hvac-form-error').removeClass('hvac-form-error'); + $('.hvac-error-message').remove(); + + // Required fields + const requiredFields = [ + { id: 'org_name', label: 'Organization Name' }, + { id: 'hq_city', label: 'Headquarters City' }, + { id: 'hq_state', label: 'Headquarters State/Province' }, + { id: 'hq_country', label: 'Headquarters Country' } + ]; + + requiredFields.forEach(field => { + const $field = $('#' + field.id); + const value = $field.val(); + + if (!value || value.trim() === '') { + isValid = false; + errors.push(field.label + ' is required'); + $field.addClass('hvac-form-error'); + $field.after('' + field.label + ' is required'); + } + }); + + // Validate email if provided + const email = $('#org_email').val(); + if (email && !isValidEmail(email)) { + isValid = false; + errors.push('Please enter a valid email address'); + $('#org_email').addClass('hvac-form-error'); + $('#org_email').after('Please enter a valid email address'); + } + + // Validate phone format if provided + const phone = $('#org_phone').val(); + if (phone && !isValidPhone(phone)) { + isValid = false; + errors.push('Please enter a valid phone number'); + $('#org_phone').addClass('hvac-form-error'); + $('#org_phone').after('Please enter a valid phone number'); + } + + // Validate website URL if provided + const website = $('#org_website').val(); + if (website && !isValidURL(website)) { + isValid = false; + errors.push('Please enter a valid website URL'); + $('#org_website').addClass('hvac-form-error'); + $('#org_website').after('Please enter a valid website URL'); + } + + return isValid; + } + + // Email validation + function isValidEmail(email) { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return re.test(email); + } + + // Phone validation + function isValidPhone(phone) { + const digits = phone.replace(/\D/g, ''); + return digits.length >= 10 && digits.length <= 15; + } + + // URL validation + function isValidURL(url) { + try { + new URL(url); + return true; + } catch (_) { + return false; + } + } + + // Show message + function showMessage(message, type = 'success') { + const messageClass = type === 'success' ? 'hvac-message-success' : 'hvac-message-error'; + const $message = $('
' + message + '
'); + + // Remove any existing messages + $('.hvac-message').remove(); + + // Add new message + $('.hvac-page-header').after($message); + + // Auto-hide success messages after 5 seconds + if (type === 'success') { + setTimeout(function() { + $message.fadeOut(function() { + $(this).remove(); + }); + }, 5000); + } + + // Scroll to top + $('html, body').animate({ + scrollTop: $('.hvac-page-header').offset().top - 100 + }, 300); + } + + // Handle organizer form submission + if ($organizerForm.length) { + $organizerForm.on('submit', function(e) { + e.preventDefault(); + + // Validate form + if (!validateOrganizerForm()) { + return false; + } + + // Disable submit button + const $submitButton = $organizerForm.find('button[type="submit"]'); + const originalText = $submitButton.text(); + $submitButton.prop('disabled', true).text('Saving...'); + + // Gather form data + const formData = { + action: 'hvac_save_organizer', + nonce: hvacOrganizers.nonce, + organizer_id: $('input[name="organizer_id"]').val(), + org_name: $('#org_name').val(), + org_description: $('#org_description').val(), + hq_city: $('#hq_city').val(), + hq_state: $('#hq_state').val(), + hq_country: $('#hq_country').val(), + org_phone: $('#org_phone').val(), + org_email: $('#org_email').val(), + org_website: $('#org_website').val(), + org_logo_id: $('#org_logo_id').val() + }; + + // Send AJAX request + $.ajax({ + url: hvacOrganizers.ajax_url, + type: 'POST', + data: formData, + success: function(response) { + if (response.success) { + showMessage(response.data.message, 'success'); + + // If creating new organizer, update form to edit mode + if (!formData.organizer_id && response.data.organizer_id) { + $('input[name="organizer_id"]').val(response.data.organizer_id); + $('.hvac-page-header h1').text('Edit Organizer'); + $('.hvac-breadcrumb').html('Trainer > Organizers > Edit'); + + // Add delete button if not present + if (!$('#hvac-delete-organizer').length) { + const deleteButton = ''; + $('.hvac-form-actions').append(deleteButton); + } + } + + // Update button text + $submitButton.text('Update Organizer'); + } else { + showMessage(response.data || 'An error occurred while saving the organizer.', 'error'); + } + }, + error: function() { + showMessage('An error occurred. Please try again.', 'error'); + }, + complete: function() { + // Re-enable submit button + $submitButton.prop('disabled', false).text(originalText); + } + }); + }); + } + + // Handle logo upload + if ($uploadButton.length) { + let mediaUploader; + + $uploadButton.on('click', function(e) { + e.preventDefault(); + + // If the media uploader already exists, open it + if (mediaUploader) { + mediaUploader.open(); + return; + } + + // Create the media uploader + mediaUploader = wp.media({ + title: 'Choose Organization Logo', + button: { + text: 'Use this logo' + }, + multiple: false, + library: { + type: 'image' + } + }); + + // When an image is selected, run a callback + mediaUploader.on('select', function() { + const attachment = mediaUploader.state().get('selection').first().toJSON(); + + // Update the logo preview + $currentLogo.html('Organization logo'); + + // Update the hidden field + $logoIdField.val(attachment.id); + + // Update button text + $uploadButton.text('Change Logo'); + + // Show remove button if not already visible + if (!$('#hvac-remove-logo').length) { + const removeBtn = ''; + $uploadButton.after(removeBtn); + } + }); + + // Open the media uploader + mediaUploader.open(); + }); + } + + // Handle logo removal + $(document).on('click', '#hvac-remove-logo', function(e) { + e.preventDefault(); + + // Clear the logo preview + $currentLogo.html('
No logo uploaded
'); + + // Clear the hidden field + $logoIdField.val(''); + + // Update button text + $uploadButton.text('Upload Logo'); + + // Remove the remove button + $(this).remove(); + }); + + // Handle organizer deletion + $(document).on('click', '#hvac-delete-organizer', function(e) { + e.preventDefault(); + + const $deleteButton = $(this); + const organizerId = $deleteButton.data('organizer-id'); + + if (!organizerId) { + showMessage('Invalid organizer ID', 'error'); + return; + } + + // Confirm deletion + if (!confirm('Are you sure you want to delete this organizer? This action cannot be undone.')) { + return; + } + + // Disable button + $deleteButton.prop('disabled', true).text('Deleting...'); + + // Send delete request + $.ajax({ + url: hvacOrganizers.ajax_url, + type: 'POST', + data: { + action: 'hvac_delete_organizer', + nonce: hvacOrganizers.nonce, + organizer_id: organizerId + }, + success: function(response) { + if (response.success) { + showMessage(response.data || 'Organizer deleted successfully.', 'success'); + + // Redirect to organizers list after 2 seconds + setTimeout(function() { + window.location.href = '/trainer/organizer/list/'; + }, 2000); + } else { + showMessage(response.data || 'Failed to delete organizer.', 'error'); + // Re-enable button + $deleteButton.prop('disabled', false).text('Delete Organizer'); + } + }, + error: function() { + showMessage('An error occurred. Please try again.', 'error'); + // Re-enable button + $deleteButton.prop('disabled', false).text('Delete Organizer'); + } + }); + }); + + // Real-time validation + $('#org_email').on('blur', function() { + const email = $(this).val(); + $('.hvac-error-message', $(this).parent()).remove(); + $(this).removeClass('hvac-form-error'); + + if (email && !isValidEmail(email)) { + $(this).addClass('hvac-form-error'); + $(this).after('Please enter a valid email address'); + } + }); + + $('#org_phone').on('blur', function() { + const phone = $(this).val(); + $('.hvac-error-message', $(this).parent()).remove(); + $(this).removeClass('hvac-form-error'); + + if (phone && !isValidPhone(phone)) { + $(this).addClass('hvac-form-error'); + $(this).after('Please enter a valid phone number'); + } + }); + + $('#org_website').on('blur', function() { + const website = $(this).val(); + $('.hvac-error-message', $(this).parent()).remove(); + $(this).removeClass('hvac-form-error'); + + if (website && !isValidURL(website)) { + $(this).addClass('hvac-form-error'); + $(this).after('Please enter a valid website URL'); + } + }); + + // Auto-format phone number + $('#org_phone').on('input', function() { + let value = $(this).val().replace(/\D/g, ''); + + if (value.length > 0) { + if (value.length <= 3) { + value = value; + } else if (value.length <= 6) { + value = value.slice(0, 3) + '-' + value.slice(3); + } else if (value.length <= 10) { + value = value.slice(0, 3) + '-' + value.slice(3, 6) + '-' + value.slice(6); + } else { + value = value.slice(0, 3) + '-' + value.slice(3, 6) + '-' + value.slice(6, 10); + } + } + + $(this).val(value); + }); +}); \ No newline at end of file diff --git a/assets/js/hvac-registration.js b/assets/js/hvac-registration.js new file mode 100644 index 00000000..3fc8dd36 --- /dev/null +++ b/assets/js/hvac-registration.js @@ -0,0 +1,363 @@ +jQuery(document).ready(function($) { + const $countrySelect = $('#user_country'); + const $stateSelect = $('#user_state'); + const $stateOtherInput = $('#user_state_other'); + const $registrationForm = $('#hvac-registration-form'); + + // Venue fields + const $createVenue = $('input[name="create_venue"]'); + const $venueDetails = $('#venue-details'); + const $venueName = $('#venue_name'); + const $businessName = $('#business_name'); + const $userCity = $('#user_city'); + const $venuePhone = $('#venue_phone'); + const $venueWebsite = $('#venue_website'); + const $businessPhone = $('#business_phone'); + const $businessWebsite = $('#business_website'); + + // Form validation helpers + function showFieldError(fieldId, message) { + const $field = $('#' + fieldId); + const $existingError = $field.siblings('.error-message'); + + if ($existingError.length) { + $existingError.text(message); + } else { + $field.after('

' + message + '

'); + } + + $field.addClass('error'); + } + + function clearFieldError(fieldId) { + const $field = $('#' + fieldId); + $field.siblings('.error-message').remove(); + $field.removeClass('error'); + } + + // Real-time email validation + $('#user_email').on('blur', function() { + const email = $(this).val(); + if (email && !isValidEmail(email)) { + showFieldError('user_email', 'Please enter a valid email address.'); + } else { + clearFieldError('user_email'); + } + }); + + $('#business_email').on('blur', function() { + const email = $(this).val(); + if (email && !isValidEmail(email)) { + showFieldError('business_email', 'Please enter a valid business email address.'); + } else { + clearFieldError('business_email'); + } + }); + + function isValidEmail(email) { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return re.test(email); + } + + // Real-time password validation + $('#user_pass').on('input blur', function() { + const password = $(this).val(); + if (password) { + const errors = []; + if (password.length < 8) { + errors.push('at least 8 characters'); + } + if (!/[A-Z]/.test(password)) { + errors.push('one uppercase letter'); + } + if (!/[a-z]/.test(password)) { + errors.push('one lowercase letter'); + } + if (!/[0-9]/.test(password)) { + errors.push('one number'); + } + + if (errors.length > 0) { + showFieldError('user_pass', 'Password must contain ' + errors.join(', ') + '.'); + } else { + clearFieldError('user_pass'); + // Check confirm password if it has a value + const confirmPass = $('#confirm_password').val(); + if (confirmPass) { + $('#confirm_password').trigger('blur'); + } + } + } + }); + + // Confirm password validation + $('#confirm_password').on('blur', function() { + const password = $('#user_pass').val(); + const confirmPassword = $(this).val(); + + if (confirmPassword && password !== confirmPassword) { + showFieldError('confirm_password', 'Passwords do not match.'); + } else { + clearFieldError('confirm_password'); + } + }); + + // URL validation for optional fields + function isValidURL(url) { + try { + new URL(url); + return true; + } catch (_) { + return false; + } + } + + $('#user_url, #user_linkedin, #business_website, #venue_website').on('blur', function() { + const url = $(this).val(); + const fieldId = $(this).attr('id'); + + if (url && !isValidURL(url)) { + const fieldName = fieldId === 'user_url' ? 'personal website' : + fieldId === 'user_linkedin' ? 'LinkedIn profile' : + fieldId === 'business_website' ? 'organization website' : + 'venue website'; + showFieldError(fieldId, 'Please enter a valid URL for your ' + fieldName + '.'); + } else { + clearFieldError(fieldId); + } + }); + + // Handle venue creation toggle + $createVenue.on('change', function() { + if ($(this).val() === 'Yes') { + $venueDetails.slideDown(); + // Auto-populate venue name if empty + updateVenueName(); + // Auto-populate venue phone and website + if (!$venuePhone.val() && $businessPhone.val()) { + $venuePhone.val($businessPhone.val()); + } + if (!$venueWebsite.val() && $businessWebsite.val()) { + $venueWebsite.val($businessWebsite.val()); + } + } else { + $venueDetails.slideUp(); + } + }); + + // Auto-populate venue name + function updateVenueName() { + if (!$venueName.val()) { + const businessName = $businessName.val(); + const city = $userCity.val(); + if (businessName && city) { + $venueName.val(businessName + ' of ' + city); + } else if (businessName) { + $venueName.val(businessName + ' Training Venue'); + } + } + } + + // Update venue name when business name or city changes + $businessName.on('blur', updateVenueName); + $userCity.on('blur', updateVenueName); + + // Copy organization info to venue fields + $businessPhone.on('blur', function() { + if (!$venuePhone.val() && $(this).val()) { + $venuePhone.val($(this).val()); + } + }); + + $businessWebsite.on('blur', function() { + if (!$venueWebsite.val() && $(this).val()) { + $venueWebsite.val($(this).val()); + } + }); + + // Form submission validation + $registrationForm.on('submit', function(e) { + let hasErrors = false; + const errors = []; + + // Check required fields + const requiredFields = [ + { id: 'user_email', name: 'Email' }, + { id: 'user_pass', name: 'Password' }, + { id: 'confirm_password', name: 'Confirm Password' }, + { id: 'first_name', name: 'First Name' }, + { id: 'last_name', name: 'Last Name' }, + { id: 'display_name', name: 'Display Name' }, + { id: 'description', name: 'Biographical Info' }, + { id: 'business_name', name: 'Organization Name' }, + { id: 'business_phone', name: 'Organization Phone' }, + { id: 'business_email', name: 'Organization Email' }, + { id: 'business_description', name: 'Organization Description' }, + { id: 'application_details', name: 'Application Details' } + ]; + + // Check venue fields if creating venue + if ($('input[name="create_venue"]:checked').val() === 'Yes') { + requiredFields.push( + { id: 'venue_name', name: 'Venue Name' }, + { id: 'venue_address', name: 'Street Address' }, + { id: 'user_country', name: 'Country' }, + { id: 'user_city', name: 'City' }, + { id: 'user_zip', name: 'Zip/Postal Code' } + ); + } + + requiredFields.forEach(field => { + const $field = $('#' + field.id); + if (!$field.val() || $field.val().trim() === '') { + hasErrors = true; + errors.push(field.name + ' is required.'); + showFieldError(field.id, field.name + ' is required.'); + } + }); + + // Check org logo + const $orgLogo = $('#org_logo'); + if ($orgLogo.length && !$orgLogo[0].files.length) { + hasErrors = true; + errors.push('Organization Logo is required.'); + showFieldError('org_logo', 'Organization Logo is required.'); + } + + // Check state/province for venue + if ($('input[name="create_venue"]:checked').val() === 'Yes') { + const country = $('#user_country').val(); + if (country) { + if (country === 'United States' || country === 'Canada') { + const state = $('#user_state').val(); + if (!state || state === '') { + hasErrors = true; + errors.push('State/Province is required.'); + showFieldError('user_state', 'State/Province is required.'); + } + } else { + const otherState = $('#user_state_other').val(); + if (!otherState || otherState.trim() === '') { + hasErrors = true; + errors.push('State/Province is required.'); + showFieldError('user_state_other', 'State/Province is required.'); + } + } + } + } + + // Check radio buttons + if (!$('input[name="create_venue"]:checked').length) { + hasErrors = true; + errors.push('Please select whether to create a training venue profile.'); + } + + if (!$('input[name="business_type"]:checked').length) { + hasErrors = true; + errors.push('Business Type is required.'); + } + + // Check checkbox groups + const checkboxGroups = [ + { name: 'training_audience[]', label: 'Training Audience' }, + { name: 'training_formats[]', label: 'Training Formats' }, + { name: 'training_locations[]', label: 'Training Locations' }, + { name: 'training_resources[]', label: 'Training Resources' } + ]; + + checkboxGroups.forEach(group => { + if (!$('input[name="' + group.name + '"]:checked').length) { + hasErrors = true; + errors.push('Please select at least one option for ' + group.label + '.'); + } + }); + + if (hasErrors) { + e.preventDefault(); + + // Show error summary at the top + let $errorSummary = $('.hvac-form-errors'); + if (!$errorSummary.length) { + $errorSummary = $(''); + $registrationForm.prepend($errorSummary); + } + + const $errorList = $errorSummary.find('ul'); + $errorList.empty(); + errors.forEach(error => { + $errorList.append('
  • ' + error + '
  • '); + }); + + // Scroll to top of form + $('html, body').animate({ + scrollTop: $('.hvac-registration-form').offset().top - 100 + }, 500); + } + }); + + // Function to populate states/provinces + function loadStates(country) { + console.log(`Loading states/provinces for ${country}`); // Keep log for debugging + $stateSelect.find('option').not('[value=""],[value="Other"]').remove(); // Clear existing options except defaults + + let options = {}; + if (country === 'United States' && typeof hvacRegistrationData !== 'undefined' && hvacRegistrationData.states) { + options = hvacRegistrationData.states; + } else if (country === 'Canada' && typeof hvacRegistrationData !== 'undefined' && hvacRegistrationData.provinces) { + options = hvacRegistrationData.provinces; + } else { + // If country is not US/CA or data is missing, ensure 'Other' is selected and input shown + $stateSelect.val('Other').trigger('change'); // Trigger change to show 'Other' input if needed + return; + } + + // Append new options + $.each(options, function(value, label) { + // Append before the 'Other' option if it exists, otherwise just append + const $otherOption = $stateSelect.find('option[value="Other"]'); + const $newOption = $('').val(value).text(label); + if ($otherOption.length > 0) { + $newOption.insertBefore($otherOption); + } else { + $stateSelect.append($newOption); + } + }); + + // Ensure the 'Other' input is hidden initially when states/provinces are loaded + $stateOtherInput.hide().val(''); + // Reset state selection to default prompt + $stateSelect.val(''); + } + + // Handle state/province field visibility based on 'Other' selection + $stateSelect.change(function() { + if ($(this).val() === 'Other') { + $stateOtherInput.show().prop('required', true); // Make required if Other is selected + } else { + $stateOtherInput.hide().val('').prop('required', false); // Hide and make not required + } + }).trigger('change'); // Trigger on load to set initial visibility + + // Handle country change to show/hide/populate state field + $countrySelect.change(function() { + const country = $(this).val(); + + if (country === 'United States' || country === 'Canada') { + loadStates(country); + $stateSelect.show().prop('required', true); // Show and require state select + $stateOtherInput.prop('required', false); // Ensure 'Other' input is not required initially + } else if (country) { + // For other countries, hide state select, select 'Other', show/require 'Other' input + $stateSelect.hide().val('Other').prop('required', false); // Hide and make not required + $stateOtherInput.show().prop('required', true); // Show and require 'Other' input + } else { + // No country selected + $stateSelect.hide().val('').prop('required', false); // Hide and make not required + $stateOtherInput.hide().val('').prop('required', false); // Hide and make not required + } + }).trigger('change'); // Trigger on load to set initial state based on pre-selected country (if any) + + // Initialize venue visibility on load + $createVenue.filter(':checked').trigger('change'); + +}); \ No newline at end of file diff --git a/assets/js/hvac-trainer-profile.js b/assets/js/hvac-trainer-profile.js new file mode 100644 index 00000000..217dd8a1 --- /dev/null +++ b/assets/js/hvac-trainer-profile.js @@ -0,0 +1,317 @@ +/** + * HVAC Trainer Profile JavaScript + * + * @package HVAC_Community_Events + * @version 2.0.0 + */ + +jQuery(document).ready(function($) { + // Cache DOM elements + const $profileForm = $('#hvac-profile-form'); + const $uploadButton = $('#hvac-upload-photo'); + const $removeButton = $('#hvac-remove-photo'); + const $photoIdField = $('#profile_photo_id'); + const $currentPhoto = $('.hvac-current-photo'); + + // Form validation + function validateProfileForm() { + let isValid = true; + const errors = []; + + // Clear previous errors + $('.hvac-form-error').removeClass('hvac-form-error'); + $('.hvac-error-message').remove(); + + // Required fields + const requiredFields = [ + { id: 'first_name', label: 'First Name' }, + { id: 'last_name', label: 'Last Name' }, + { id: 'display_name', label: 'Display Name' }, + { id: 'email', label: 'Email Address' } + ]; + + requiredFields.forEach(field => { + const $field = $('#' + field.id); + const value = $field.val(); + + if (!value || value.trim() === '') { + isValid = false; + errors.push(field.label + ' is required'); + $field.addClass('hvac-form-error'); + $field.after('' + field.label + ' is required'); + } + }); + + // Validate email + const email = $('#email').val(); + if (email && !isValidEmail(email)) { + isValid = false; + errors.push('Please enter a valid email address'); + $('#email').addClass('hvac-form-error'); + $('#email').after('Please enter a valid email address'); + } + + // Validate phone format if provided + const phone = $('#phone').val(); + if (phone && !isValidPhone(phone)) { + isValid = false; + errors.push('Please enter a valid phone number'); + $('#phone').addClass('hvac-form-error'); + $('#phone').after('Please enter a valid phone number'); + } + + // Validate URLs if provided + const website = $('#website').val(); + if (website && !isValidURL(website)) { + isValid = false; + errors.push('Please enter a valid website URL'); + $('#website').addClass('hvac-form-error'); + $('#website').after('Please enter a valid website URL'); + } + + const linkedin = $('#linkedin').val(); + if (linkedin && !isValidURL(linkedin)) { + isValid = false; + errors.push('Please enter a valid LinkedIn URL'); + $('#linkedin').addClass('hvac-form-error'); + $('#linkedin').after('Please enter a valid LinkedIn URL'); + } + + // Validate years of experience + const years = $('#years_experience').val(); + if (years && (years < 0 || years > 50)) { + isValid = false; + errors.push('Years of experience must be between 0 and 50'); + $('#years_experience').addClass('hvac-form-error'); + $('#years_experience').after('Must be between 0 and 50'); + } + + return isValid; + } + + // Email validation + function isValidEmail(email) { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return re.test(email); + } + + // Phone validation + function isValidPhone(phone) { + const digits = phone.replace(/\D/g, ''); + return digits.length >= 10 && digits.length <= 15; + } + + // URL validation + function isValidURL(url) { + try { + new URL(url); + return true; + } catch (_) { + return false; + } + } + + // Show message + function showMessage(message, type = 'success') { + const messageClass = type === 'success' ? 'hvac-message-success' : 'hvac-message-error'; + const $message = $('
    ' + message + '
    '); + + // Remove any existing messages + $('.hvac-message').remove(); + + // Add new message + $('.hvac-page-header').after($message); + + // Auto-hide success messages after 5 seconds + if (type === 'success') { + setTimeout(function() { + $message.fadeOut(function() { + $(this).remove(); + }); + }, 5000); + } + + // Scroll to top + $('html, body').animate({ + scrollTop: $('.hvac-page-header').offset().top - 100 + }, 300); + } + + // Handle profile form submission + if ($profileForm.length) { + $profileForm.on('submit', function(e) { + e.preventDefault(); + + // Validate form + if (!validateProfileForm()) { + return false; + } + + // Disable submit button + const $submitButton = $profileForm.find('button[type="submit"]'); + const originalText = $submitButton.text(); + $submitButton.prop('disabled', true).text('Saving...'); + + // Gather form data + const formData = { + action: 'hvac_update_profile', + nonce: hvacProfile.nonce, + first_name: $('#first_name').val(), + last_name: $('#last_name').val(), + display_name: $('#display_name').val(), + email: $('#email').val(), + phone: $('#phone').val(), + description: $('#description').val(), + city: $('#city').val(), + state: $('#state').val(), + country: $('#country').val(), + years_experience: $('#years_experience').val(), + certifications: $('#certifications').val(), + website: $('#website').val(), + linkedin: $('#linkedin').val(), + profile_photo_id: $('#profile_photo_id').val() + }; + + // Send AJAX request + $.ajax({ + url: hvacProfile.ajax_url, + type: 'POST', + data: formData, + success: function(response) { + if (response.success) { + showMessage(response.data || 'Profile updated successfully.', 'success'); + } else { + showMessage(response.data || 'An error occurred while updating your profile.', 'error'); + } + }, + error: function() { + showMessage('An error occurred. Please try again.', 'error'); + }, + complete: function() { + // Re-enable submit button + $submitButton.prop('disabled', false).text(originalText); + } + }); + }); + } + + // Handle photo upload + if ($uploadButton.length) { + let mediaUploader; + + $uploadButton.on('click', function(e) { + e.preventDefault(); + + // If the media uploader already exists, open it + if (mediaUploader) { + mediaUploader.open(); + return; + } + + // Create the media uploader + mediaUploader = wp.media({ + title: 'Choose Profile Photo', + button: { + text: 'Use this photo' + }, + multiple: false, + library: { + type: 'image' + } + }); + + // When an image is selected, run a callback + mediaUploader.on('select', function() { + const attachment = mediaUploader.state().get('selection').first().toJSON(); + + // Update the photo preview + $currentPhoto.html('Profile photo'); + + // Update the hidden field + $photoIdField.val(attachment.id); + + // Update button text + $uploadButton.text('Change Photo'); + + // Show remove button if not already visible + if (!$removeButton.length) { + const removeBtn = ''; + $uploadButton.after(removeBtn); + } + }); + + // Open the media uploader + mediaUploader.open(); + }); + } + + // Handle photo removal + $(document).on('click', '#hvac-remove-photo', function(e) { + e.preventDefault(); + + // Clear the photo preview + $currentPhoto.html('
    No photo uploaded
    '); + + // Clear the hidden field + $photoIdField.val(''); + + // Update button text + $uploadButton.text('Upload Photo'); + + // Remove the remove button + $(this).remove(); + }); + + // Real-time validation + $('#email').on('blur', function() { + const email = $(this).val(); + $('.hvac-error-message', $(this).parent()).remove(); + $(this).removeClass('hvac-form-error'); + + if (email && !isValidEmail(email)) { + $(this).addClass('hvac-form-error'); + $(this).after('Please enter a valid email address'); + } + }); + + $('#phone').on('blur', function() { + const phone = $(this).val(); + $('.hvac-error-message', $(this).parent()).remove(); + $(this).removeClass('hvac-form-error'); + + if (phone && !isValidPhone(phone)) { + $(this).addClass('hvac-form-error'); + $(this).after('Please enter a valid phone number'); + } + }); + + $('#website, #linkedin').on('blur', function() { + const url = $(this).val(); + $('.hvac-error-message', $(this).parent()).remove(); + $(this).removeClass('hvac-form-error'); + + if (url && !isValidURL(url)) { + $(this).addClass('hvac-form-error'); + $(this).after('Please enter a valid URL'); + } + }); + + // Auto-format phone number + $('#phone').on('input', function() { + let value = $(this).val().replace(/\D/g, ''); + + if (value.length > 0) { + if (value.length <= 3) { + value = value; + } else if (value.length <= 6) { + value = value.slice(0, 3) + '-' + value.slice(3); + } else if (value.length <= 10) { + value = value.slice(0, 3) + '-' + value.slice(3, 6) + '-' + value.slice(6); + } else { + value = value.slice(0, 3) + '-' + value.slice(3, 6) + '-' + value.slice(6, 10); + } + } + + $(this).val(value); + }); +}); \ No newline at end of file diff --git a/assets/js/hvac-venues.js b/assets/js/hvac-venues.js new file mode 100644 index 00000000..d69b90e2 --- /dev/null +++ b/assets/js/hvac-venues.js @@ -0,0 +1,283 @@ +/** + * HVAC Venues JavaScript + * + * @package HVAC_Community_Events + * @version 2.0.0 + */ + +jQuery(document).ready(function($) { + // Cache DOM elements + const $venueForm = $('#hvac-venue-form'); + const $deleteButton = $('#hvac-delete-venue'); + const $filterForm = $('.hvac-filter-form'); + + // Form validation + function validateVenueForm() { + let isValid = true; + const errors = []; + + // Clear previous errors + $('.hvac-form-error').removeClass('hvac-form-error'); + $('.hvac-error-message').remove(); + + // Required fields + const requiredFields = [ + { id: 'venue_name', label: 'Venue Name' }, + { id: 'venue_address', label: 'Street Address' }, + { id: 'venue_city', label: 'City' }, + { id: 'venue_state', label: 'State/Province' }, + { id: 'venue_zip', label: 'Zip/Postal Code' }, + { id: 'venue_country', label: 'Country' } + ]; + + requiredFields.forEach(field => { + const $field = $('#' + field.id); + const value = $field.val(); + + if (!value || value.trim() === '') { + isValid = false; + errors.push(field.label + ' is required'); + $field.addClass('hvac-form-error'); + $field.after('' + field.label + ' is required'); + } + }); + + // Validate phone format if provided + const phone = $('#venue_phone').val(); + if (phone && !isValidPhone(phone)) { + isValid = false; + errors.push('Please enter a valid phone number'); + $('#venue_phone').addClass('hvac-form-error'); + $('#venue_phone').after('Please enter a valid phone number'); + } + + // Validate website URL if provided + const website = $('#venue_website').val(); + if (website && !isValidURL(website)) { + isValid = false; + errors.push('Please enter a valid website URL'); + $('#venue_website').addClass('hvac-form-error'); + $('#venue_website').after('Please enter a valid website URL'); + } + + return isValid; + } + + // Phone validation + function isValidPhone(phone) { + // Remove all non-digits + const digits = phone.replace(/\D/g, ''); + // Check if it's 10 or 11 digits (US/Canada) + return digits.length >= 10 && digits.length <= 15; + } + + // URL validation + function isValidURL(url) { + try { + new URL(url); + return true; + } catch (_) { + return false; + } + } + + // Show success message + function showMessage(message, type = 'success') { + const messageClass = type === 'success' ? 'hvac-message-success' : 'hvac-message-error'; + const $message = $('
    ' + message + '
    '); + + // Remove any existing messages + $('.hvac-message').remove(); + + // Add new message + $('.hvac-page-header').after($message); + + // Auto-hide success messages after 5 seconds + if (type === 'success') { + setTimeout(function() { + $message.fadeOut(function() { + $(this).remove(); + }); + }, 5000); + } + + // Scroll to top + $('html, body').animate({ + scrollTop: $('.hvac-page-header').offset().top - 100 + }, 300); + } + + // Handle venue form submission + if ($venueForm.length) { + $venueForm.on('submit', function(e) { + e.preventDefault(); + + // Validate form + if (!validateVenueForm()) { + return false; + } + + // Disable submit button + const $submitButton = $venueForm.find('button[type="submit"]'); + const originalText = $submitButton.text(); + $submitButton.prop('disabled', true).text('Saving...'); + + // Gather form data + const formData = { + action: 'hvac_save_venue', + nonce: hvacVenues.nonce, + venue_id: $('input[name="venue_id"]').val(), + venue_name: $('#venue_name').val(), + venue_description: $('#venue_description').val(), + venue_address: $('#venue_address').val(), + venue_city: $('#venue_city').val(), + venue_state: $('#venue_state').val(), + venue_zip: $('#venue_zip').val(), + venue_country: $('#venue_country').val(), + venue_phone: $('#venue_phone').val(), + venue_website: $('#venue_website').val() + }; + + // Send AJAX request + $.ajax({ + url: hvacVenues.ajax_url, + type: 'POST', + data: formData, + success: function(response) { + if (response.success) { + showMessage(response.data.message, 'success'); + + // If creating new venue, update form to edit mode + if (!formData.venue_id && response.data.venue_id) { + $('input[name="venue_id"]').val(response.data.venue_id); + $('.hvac-page-header h1').text('Edit Venue'); + $('.hvac-breadcrumb').html('Trainer > Venues > Edit'); + + // Add delete button if not present + if (!$('#hvac-delete-venue').length) { + const deleteButton = ''; + $('.hvac-form-actions').append(deleteButton); + } + } + + // Update button text + $submitButton.text('Update Venue'); + } else { + showMessage(response.data || 'An error occurred while saving the venue.', 'error'); + } + }, + error: function() { + showMessage('An error occurred. Please try again.', 'error'); + }, + complete: function() { + // Re-enable submit button + $submitButton.prop('disabled', false).text(originalText); + } + }); + }); + } + + // Handle venue deletion + $(document).on('click', '#hvac-delete-venue', function(e) { + e.preventDefault(); + + const $deleteButton = $(this); + const venueId = $deleteButton.data('venue-id'); + + if (!venueId) { + showMessage('Invalid venue ID', 'error'); + return; + } + + // Confirm deletion + if (!confirm('Are you sure you want to delete this venue? This action cannot be undone.')) { + return; + } + + // Disable button + $deleteButton.prop('disabled', true).text('Deleting...'); + + // Send delete request + $.ajax({ + url: hvacVenues.ajax_url, + type: 'POST', + data: { + action: 'hvac_delete_venue', + nonce: hvacVenues.nonce, + venue_id: venueId + }, + success: function(response) { + if (response.success) { + showMessage(response.data || 'Venue deleted successfully.', 'success'); + + // Redirect to venues list after 2 seconds + setTimeout(function() { + window.location.href = '/trainer/venue/list/'; + }, 2000); + } else { + showMessage(response.data || 'Failed to delete venue.', 'error'); + // Re-enable button + $deleteButton.prop('disabled', false).text('Delete Venue'); + } + }, + error: function() { + showMessage('An error occurred. Please try again.', 'error'); + // Re-enable button + $deleteButton.prop('disabled', false).text('Delete Venue'); + } + }); + }); + + // Handle filter form submission + if ($filterForm.length) { + // Prevent form submission on enter in search field + $filterForm.find('input[name="search"]').on('keypress', function(e) { + if (e.which === 13) { + e.preventDefault(); + $filterForm.find('button[type="submit"]').click(); + } + }); + } + + // Real-time validation + $('#venue_phone').on('blur', function() { + const phone = $(this).val(); + $('.hvac-error-message', $(this).parent()).remove(); + $(this).removeClass('hvac-form-error'); + + if (phone && !isValidPhone(phone)) { + $(this).addClass('hvac-form-error'); + $(this).after('Please enter a valid phone number'); + } + }); + + $('#venue_website').on('blur', function() { + const website = $(this).val(); + $('.hvac-error-message', $(this).parent()).remove(); + $(this).removeClass('hvac-form-error'); + + if (website && !isValidURL(website)) { + $(this).addClass('hvac-form-error'); + $(this).after('Please enter a valid website URL'); + } + }); + + // Auto-format phone number + $('#venue_phone').on('input', function() { + let value = $(this).val().replace(/\D/g, ''); + + if (value.length > 0) { + if (value.length <= 3) { + value = value; + } else if (value.length <= 6) { + value = value.slice(0, 3) + '-' + value.slice(3); + } else if (value.length <= 10) { + value = value.slice(0, 3) + '-' + value.slice(3, 6) + '-' + value.slice(6); + } else { + value = value.slice(0, 3) + '-' + value.slice(3, 6) + '-' + value.slice(6, 10); + } + } + + $(this).val(value); + }); +}); \ No newline at end of file diff --git a/docs/trainer_page_refactor_30July2025.md b/docs/trainer_page_refactor_30July2025.md new file mode 100644 index 00000000..2c117958 --- /dev/null +++ b/docs/trainer_page_refactor_30July2025.md @@ -0,0 +1,41 @@ +We need to make a few major updates to the plugin. + +We will be refactoring the Trainer Registration Page. + 1. First, we want to edit the "Personal Information" section at the top of the form to be better aligned with our desired Trainer Profile page (which is associated with the User post type):[ + - move the "Application Details" question to this section + - Ensure that the custom fields in this section are actually created as custom fields in the User post type. + 2. The Existing "Business Information" section should be renamed to "Training Organization Information". This will align with the TEC post type called "Organizer". + - It will list the following pre-existing fields from the TEC plugin ("*" means required): Org Name *, Org Phone *, Org Email *, Org Website, Org Description *, Org Logo * (featured image of the Organizer). + - We want to add new custom fields called "Org Headquarters City", "Org Headquarters State", and "Org Headquarters Country", + - We also want all custom fields moved from the "Training Information" into this new "Training Organization Information" section. These fields likely do not currently exist as field types, so we need to create them. +3. We must modify how "Venue" records are created. A Venue is a special post type from The Events Calendar Plugin, and all users with the HVAC Trainer role should be associated with at least 1 Venue (created during registration). Here's the changes we want in this section: + - The "Address Information" extion should be renamed to "Training Venue Information", and it should be moved to the end of the form. + - This section already has a question which asks "Create Training Venue Profile?", which is meant to auto-populate the training venue profile. It should default to "Yes". + - The following custom fields fields will be conditionally visible when "Yes" is selected, and though most are auto-populated from the Org section above, the user should be able to edit them: Venue Name (defaults to [Org Name] + " of " [Org City]), Street Address, City, State/Province, Country, Postal Code, Phone, Website + +Next, we want to update/add pages which are accessible to logged in trainers. +1. "Training Venues" Page (/trainer/venue/list): This page will allow the users to view a list of all training venues, even ones they didn't create. This should be in table format with similar filters/pagination as the dashboard page. If they are the author of a venue, then they there should be an "Edit" button beside that venue in the table. +2. "Manage Venue" Page (/trainer/venue/manage): This should have similar functionality to the "Manage Event" page, but will allow the user to create new Venuws or edit Venues which they are the author of, including all default and custom fields related to the Venue post type. +3. "Trainer Profile" Page (/trainer/profile and /trainer/profile/edit): This will be where trainers can view all default and custom fields related to the User post type. +4. "Training Organizers" Page (/trainer/organizer/list): This should have similar functionality to the "Manage Event" page, but will allow the user to create new Venuws or edit Organizers which they are the author of, including all default and custom fields related to the Organizer post type. +5. "Manage Venue" Page (/trainer/organizer/manage): This should have similar functionality to the "Manage Event" page, but will allow the user to create new Venuws or edit Organizers which they are the author of, including all default and custom fields related to the Organizer post type. + +Finally, We should refactor the navigation, URL structure, and add Breadcrumbs. +- URL structure should generally align with the menu structure and we should also add breadcrumbs at the top of each page which also match the same patterns (trainer/dashboard/ = Trainer > Events > Dashboard; trainer/certificates/reports = Trainer > Certificates > Reports; +- Instead of having a series of custom buttons, we should create a native wordpress "Menu". It should be a Secondary Menu only available to Logged in users with the HVAC Trainer Role, and will only be shown on the custom pages from our custom plugin. As follows: + Events + ├─ Dashboard + ├─ New Event + Certificates + ├─ Reports + ├─ New Certificate + Customize + ├─ Personal Profile + ├─ Training Organizers + │ ├─ Add New Organizer (links to the Manage Organizer Page) + ├─ Training Venues + │ ├─ Add New Venue (links to the Manage Venue Page) + Logout + Help + +Your first task is to perform a detailed review of the existing page structure and ultrathink a plan to acheive the desired changes. Pay special attention to all of the requisite custom fields and ensure that you follow best practices in creating the field and associating them with the appropriate post types. If there are inherent wordpress limitations which prevent you from achieving your directives, inform the user before making your plan. \ No newline at end of file diff --git a/includes/class-hvac-organizers.php b/includes/class-hvac-organizers.php new file mode 100644 index 00000000..5bdfe2c1 --- /dev/null +++ b/includes/class-hvac-organizers.php @@ -0,0 +1,575 @@ + admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('hvac_organizers_nonce') + )); + + // Enqueue media uploader for logo upload + if (is_page('trainer/organizer/manage')) { + wp_enqueue_media(); + } + } + } + + /** + * Render organizers list + */ + public function render_organizers_list() { + if (!is_user_logged_in() || !current_user_can('hvac_trainer')) { + return '

    You must be logged in as a trainer to view this page.

    '; + } + + ob_start(); + ?> +
    +
    +

    Training Organizers

    + Add New Organizer +
    + +
    + Trainer > Organizers > List +
    + + render_organizers_table(); ?> +
    + class_exists('Tribe__Events__Main') ? Tribe__Events__Main::ORGANIZER_POST_TYPE : 'tribe_organizer', + 'posts_per_page' => $per_page, + 'offset' => $offset, + 'orderby' => 'title', + 'order' => 'ASC', + 'post_status' => 'publish', + 'author' => $current_user_id // Only show organizers created by this user + ); + + // Filter handling + if (!empty($_GET['search'])) { + $query_args['s'] = sanitize_text_field($_GET['search']); + } + + // Get organizers + $organizers_query = new WP_Query($query_args); + + // Get total count for pagination + $total_organizers = $organizers_query->found_posts; + $total_pages = ceil($total_organizers / $per_page); + + ?> +
    +
    +
    +
    + +
    +
    + + Clear +
    +
    +
    +
    + +
    + + + + + + + + + + + + + have_posts()) { + while ($organizers_query->have_posts()) { + $organizers_query->the_post(); + $organizer_id = get_the_ID(); + + // Get organizer meta + $phone = get_post_meta($organizer_id, '_OrganizerPhone', true); + $email = get_post_meta($organizer_id, '_OrganizerEmail', true); + $website = get_post_meta($organizer_id, '_OrganizerWebsite', true); + + // Get headquarters location + $hq_city = get_post_meta($organizer_id, '_hvac_headquarters_city', true); + $hq_state = get_post_meta($organizer_id, '_hvac_headquarters_state', true); + $hq_country = get_post_meta($organizer_id, '_hvac_headquarters_country', true); + $hq_parts = array_filter(array($hq_city, $hq_state, $hq_country)); + $headquarters = implode(', ', $hq_parts); + + ?> + + + + + + + + + + + + + + +
    LogoOrganization NameHeadquartersContactWebsiteActions
    + + + + + + +
    + +
    + + Visit Website + + Not specified + + + Edit +
    No organizers found.
    +
    + + 1): ?> +
    + add_query_arg('paged', '%#%'), + 'format' => '', + 'current' => $page, + 'total' => $total_pages, + 'prev_text' => '« Previous', + 'next_text' => 'Next »' + )); + ?> +
    + + You must be logged in as a trainer to view this page.

    '; + } + + $organizer_id = isset($_GET['organizer_id']) ? intval($_GET['organizer_id']) : 0; + $organizer = null; + + if ($organizer_id) { + $organizer = get_post($organizer_id); + + // Check if user can edit this organizer + if (!$organizer || $organizer->post_author != get_current_user_id()) { + return '

    You do not have permission to edit this organizer.

    '; + } + } + + ob_start(); + ?> +
    +
    +

    +
    + +
    + Trainer > + Organizers > + +
    + +
    + + + +
    +

    Organization Logo

    +
    + +
    + + + + + +
    +

    Recommended size: 300x300px. Maximum file size: 2MB.

    +
    +
    + +
    +

    Organization Information

    + +
    + + +
    + +
    + + +
    +
    + +
    +

    Headquarters Location

    + +
    + + +
    + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +

    Contact Information

    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + +
    + + Cancel + + + + +
    +
    +
    + post_author != get_current_user_id()) { + wp_send_json_error('You do not have permission to edit this organizer.'); + } + } + + // Prepare organizer data + $organizer_data = array( + 'Organizer' => sanitize_text_field($_POST['org_name']), + 'Description' => wp_kses_post($_POST['org_description']), + 'Phone' => sanitize_text_field($_POST['org_phone']), + 'Email' => sanitize_email($_POST['org_email']), + 'Website' => esc_url_raw($_POST['org_website']) + ); + + if ($organizer_id) { + $organizer_data['ID'] = $organizer_id; + $result = function_exists('tribe_update_organizer') ? + tribe_update_organizer($organizer_id, $organizer_data) : + wp_update_post(array( + 'ID' => $organizer_id, + 'post_title' => $organizer_data['Organizer'], + 'post_content' => $organizer_data['Description'] + )); + } else { + $organizer_data['post_status'] = 'publish'; + $organizer_data['post_author'] = get_current_user_id(); + $result = function_exists('tribe_create_organizer') ? + tribe_create_organizer($organizer_data) : + wp_insert_post(array( + 'post_type' => 'tribe_organizer', + 'post_title' => $organizer_data['Organizer'], + 'post_content' => $organizer_data['Description'], + 'post_status' => 'publish', + 'post_author' => get_current_user_id() + )); + } + + if (is_wp_error($result)) { + wp_send_json_error($result->get_error_message()); + } + + // Update custom meta fields + $organizer_id = $organizer_id ?: $result; + + update_post_meta($organizer_id, '_hvac_headquarters_city', sanitize_text_field($_POST['hq_city'])); + update_post_meta($organizer_id, '_hvac_headquarters_state', sanitize_text_field($_POST['hq_state'])); + update_post_meta($organizer_id, '_hvac_headquarters_country', sanitize_text_field($_POST['hq_country'])); + + // Update phone, email, website meta + update_post_meta($organizer_id, '_OrganizerPhone', sanitize_text_field($_POST['org_phone'])); + update_post_meta($organizer_id, '_OrganizerEmail', sanitize_email($_POST['org_email'])); + update_post_meta($organizer_id, '_OrganizerWebsite', esc_url_raw($_POST['org_website'])); + + // Handle logo + if (isset($_POST['org_logo_id'])) { + $logo_id = intval($_POST['org_logo_id']); + if ($logo_id) { + set_post_thumbnail($organizer_id, $logo_id); + } else { + delete_post_thumbnail($organizer_id); + } + } + + // Update user's organizer_id if this is their first organizer + $user_organizer_id = get_user_meta(get_current_user_id(), 'organizer_id', true); + if (!$user_organizer_id) { + update_user_meta(get_current_user_id(), 'organizer_id', $organizer_id); + } + + wp_send_json_success(array( + 'message' => $organizer_id ? 'Organizer updated successfully.' : 'Organizer created successfully.', + 'organizer_id' => $organizer_id + )); + } + + /** + * AJAX handler for deleting organizer + */ + public function ajax_delete_organizer() { + check_ajax_referer('hvac_organizers_nonce', 'nonce'); + + if (!current_user_can('hvac_trainer')) { + wp_send_json_error('Unauthorized'); + } + + $organizer_id = isset($_POST['organizer_id']) ? intval($_POST['organizer_id']) : 0; + + if (!$organizer_id) { + wp_send_json_error('Invalid organizer ID'); + } + + $organizer = get_post($organizer_id); + if (!$organizer || $organizer->post_author != get_current_user_id()) { + wp_send_json_error('You do not have permission to delete this organizer.'); + } + + // Check if organizer is being used by any events + $events_using_organizer = get_posts(array( + 'post_type' => class_exists('Tribe__Events__Main') ? Tribe__Events__Main::POSTTYPE : 'tribe_events', + 'meta_query' => array( + array( + 'key' => '_EventOrganizerID', + 'value' => $organizer_id, + 'compare' => '=' + ) + ), + 'posts_per_page' => 1 + )); + + if (!empty($events_using_organizer)) { + wp_send_json_error('Cannot delete organizer. It is being used by one or more events.'); + } + + $result = wp_trash_post($organizer_id); + + if ($result) { + // If this was the user's primary organizer, clear it + $user_organizer_id = get_user_meta(get_current_user_id(), 'organizer_id', true); + if ($user_organizer_id == $organizer_id) { + delete_user_meta(get_current_user_id(), 'organizer_id'); + } + + wp_send_json_success('Organizer deleted successfully.'); + } else { + wp_send_json_error('Failed to delete organizer.'); + } + } + + /** + * AJAX handler for uploading organization logo + */ + public function ajax_upload_org_logo() { + check_ajax_referer('hvac_organizers_nonce', 'nonce'); + + if (!current_user_can('hvac_trainer')) { + wp_send_json_error('Unauthorized'); + } + + if (!isset($_FILES['org_logo'])) { + wp_send_json_error('No file uploaded'); + } + + require_once(ABSPATH . 'wp-admin/includes/image.php'); + require_once(ABSPATH . 'wp-admin/includes/file.php'); + require_once(ABSPATH . 'wp-admin/includes/media.php'); + + $attachment_id = media_handle_upload('org_logo', 0); + + if (is_wp_error($attachment_id)) { + wp_send_json_error($attachment_id->get_error_message()); + } + + wp_send_json_success(array( + 'attachment_id' => $attachment_id, + 'url' => wp_get_attachment_image_url($attachment_id, 'medium') + )); + } +} \ No newline at end of file diff --git a/includes/class-hvac-plugin.php b/includes/class-hvac-plugin.php index 0beee21c..d932b683 100644 --- a/includes/class-hvac-plugin.php +++ b/includes/class-hvac-plugin.php @@ -97,6 +97,9 @@ class HVAC_Plugin { 'class-hvac-trainer-status.php', 'class-hvac-access-control.php', 'class-hvac-registration.php', + 'class-hvac-venues.php', + 'class-hvac-trainer-profile-manager.php', + 'class-hvac-organizers.php', 'class-hvac-manage-event.php', 'class-hvac-event-summary.php', 'class-hvac-trainer-profile.php', @@ -301,6 +304,21 @@ class HVAC_Plugin { new HVAC_Registration(); } + // Initialize venues management + if (class_exists('HVAC_Venues')) { + new HVAC_Venues(); + } + + // Initialize trainer profile manager + if (class_exists('HVAC_Trainer_Profile_Manager')) { + new HVAC_Trainer_Profile_Manager(); + } + + // Initialize organizers management + if (class_exists('HVAC_Organizers')) { + new HVAC_Organizers(); + } + // Initialize event management if (class_exists('HVAC_Manage_Event')) { new HVAC_Manage_Event(); diff --git a/includes/class-hvac-registration.backup.php b/includes/class-hvac-registration.backup.php new file mode 100644 index 00000000..7fd8a32f --- /dev/null +++ b/includes/class-hvac-registration.backup.php @@ -0,0 +1,1416 @@ +'; + if (!empty($errors['transient'])) echo '

    ' . esc_html($errors['transient']) . '

    '; + // Nonce errors should ideally be caught in admin-post, but display if somehow set + if (!empty($errors['nonce'])) echo '

    ' . esc_html($errors['nonce']) . '

    '; +// error_log('[HVAC REG DEBUG] render_registration_form: Errors before display_form_html: ' . print_r($errors, true)); + if (!empty($errors['account'])) echo '

    ' . esc_html($errors['account']) . '

    '; + echo ''; + } + + // Display the form HTML, passing retrieved errors and submitted data + // No success message here anymore, success leads to redirect + $this->display_form_html($submitted_data, $errors); + + return ob_get_clean(); + // --- End Render Form --- + } + + /** + * Processes the registration form submission via admin-post. + * Handles validation, user creation, notifications, and redirects. + */ + public function process_registration_submission() { + + $errors = []; + $submitted_data = $_POST; // Capture submitted data early for potential repopulation + $registration_page_url = home_url('/trainer/registration/'); // Updated to hierarchical URL + + // --- Verify Nonce --- + if (!isset($_POST['hvac_registration_nonce']) || !wp_verify_nonce($_POST['hvac_registration_nonce'], 'hvac_trainer_registration')) { + $errors['nonce'] = 'Security check failed. Please try submitting the form again.'; + + $this->redirect_with_errors($errors, $submitted_data, $registration_page_url); + // No need for return/exit here, redirect_with_errors exits. + } + + // --- File Upload Handling --- + $profile_image_data = null; + if (isset($_FILES['profile_image']) && $_FILES['profile_image']['error'] !== UPLOAD_ERR_NO_FILE) { + if ($_FILES['profile_image']['error'] === UPLOAD_ERR_OK) { + // Check if it's actually an uploaded file + if (!is_uploaded_file($_FILES['profile_image']['tmp_name'])) { + $errors['profile_image'] = 'File upload error (invalid temp file).'; + + } else { + $allowed_types = ['image/jpeg', 'image/png', 'image/gif']; + // Use wp_check_filetype on the actual file name for extension check + // Use finfo_file or getimagesize on tmp_name for actual MIME type check for better security + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime_type = finfo_file($finfo, $_FILES['profile_image']['tmp_name']); + finfo_close($finfo); + + if (!in_array($mime_type, $allowed_types)) { + $errors['profile_image'] = 'Invalid file type detected (' . esc_html($mime_type) . '). Please upload a JPG, PNG, or GIF.'; + + } else { + $profile_image_data = $_FILES['profile_image']; // Store the whole $_FILES entry + + } + } +// error_log('[HVAC REG DEBUG] process_registration_submission: Errors after validation merge: ' . print_r($errors, true)); + } else { + $errors['profile_image'] = 'There was an error uploading the profile image. Code: ' . $_FILES['profile_image']['error']; + +// error_log('[HVAC REG DEBUG] process_registration_submission: Checking if errors is empty. Result: ' . (empty($errors) ? 'Yes' : 'No')); + } + } + // --- End File Upload Handling --- + + // Validate the rest of the form data + $validation_errors = $this->validate_registration($submitted_data); + $errors = array_merge($errors, $validation_errors); // Combine file errors and validation errors + + // --- Process if No Errors --- + if (empty($errors)) { + + $user_id = $this->create_trainer_account($submitted_data, $profile_image_data); + + if (is_wp_error($user_id)) { + $errors['account'] = $user_id->get_error_message(); +// error_log('[HVAC REG DEBUG] Account creation WP_Error in admin-post: ' . $user_id->get_error_message()); + $this->redirect_with_errors($errors, $submitted_data, $registration_page_url); + // No need for return/exit here + } elseif ($user_id) { + + // Send admin notification using the approval workflow + if (class_exists('HVAC_Approval_Workflow')) { + $approval_workflow = new HVAC_Approval_Workflow(); + $approval_workflow->send_new_registration_notification($user_id, $submitted_data); + } else { + // Fallback to old method + $this->send_admin_notification($user_id, $submitted_data); + } + + // --- Success Redirect --- + $success_redirect_url = home_url('/registration-pending/'); // URL from E2E test + + wp_safe_redirect($success_redirect_url); + exit; // Important after redirect + + } else { + // This case should ideally not happen if wp_insert_user works correctly + $errors['account'] = 'An unknown error occurred during registration. Please contact support.'; + + $this->redirect_with_errors($errors, $submitted_data, $registration_page_url); + // No need for return/exit here + } + } else { +// error_log('[HVAC REG DEBUG] Validation errors found in admin-post: ' . print_r($errors, true)); + $this->redirect_with_errors($errors, $submitted_data, $registration_page_url); + // No need for return/exit here + } + } + + /** + * Helper function to store errors/data in transient and redirect back to the form page. +// error_log('[HVAC REG DEBUG] redirect_with_errors: Preparing transient. Key: ' . $transient_key . ' Data: ' . print_r($transient_data, true)); + * + * @param array $errors Array of error messages. + * @param array $data Submitted form data. + * @param string $redirect_url The URL to redirect back to. + */ + private function redirect_with_errors($errors, $data, $redirect_url) { + $transient_id = uniqid(); // Generate unique ID for transient key + $transient_key = self::TRANSIENT_PREFIX . $transient_id; + $transient_data = [ + 'errors' => $errors, + 'data' => $data, // Store submitted data to repopulate form + ]; + // Store for 5 minutes + set_transient($transient_key, $transient_data, MINUTE_IN_SECONDS * 5); + + // Add query arguments to the redirect URL + $redirect_url = add_query_arg([ + 'reg_error' => '1', + 'tid' => $transient_id, + ], $redirect_url); + + wp_safe_redirect($redirect_url); + exit; // Stop execution after redirect + } + + /** + * Displays the actual form HTML. + * Receives submitted data and errors as arguments (potentially retrieved from transient). + */ + private function display_form_html($data = [], $errors = []) { + // Ensure $data and $errors are arrays, even if transient failed + $data = is_array($data) ? $data : []; + $errors = is_array($errors) ? $errors : []; + ?> +
    +

    HVAC Trainer Registration

    +

    By submitting this form, you will be creating an account in the Upskill HVAC online event system. Once approved, you will be able to login to the trainer portal to manage your profile and event listings.

    + + + + + + + +
    + + + + + +
    +

    Account Information

    +
    + + + ' . esc_html($errors['user_email']) . '

    '; ?> +
    +
    +
    + + + Password must be at least 8 characters long, and include at least one uppercase letter, one lowercase letter, and one number. + ' . esc_html($errors['user_pass']) . '

    '; ?> +
    +
    + + + ' . esc_html($errors['confirm_password']) . '

    '; ?> +
    +
    + + +
    + + +
    +

    Personal Information

    +
    +
    + + + ' . esc_html($errors['first_name']) . '

    '; ?> +
    +
    + + + ' . esc_html($errors['last_name']) . '

    '; ?> +
    +
    +
    + + + This will be the name displayed to other users on the site. + ' . esc_html($errors['display_name']) . '

    '; ?> +
    +
    +
    + + + ' . esc_html($errors['user_url']) . '

    '; ?> +
    +
    + + + ' . esc_html($errors['user_linkedin']) . '

    '; ?> +
    +
    +
    + + + Enter your abbreviated accreditations separated by commas. +
    +
    + + + A short bio about yourself. This will be displayed on your profile page. + ' . esc_html($errors['description']) . '

    '; ?> +
    +
    + + Please explain why you want to create a training account on Upskill HVAC. + + ' . esc_html($errors['application_details']) . '

    '; ?> +
    +
    + + + Please attach a .jpg, .png, or .gif image. By attaching an image to use as your profile picture you assert that you have rights to use the image and it is not illegal, pornographic or violent in any way. This will be displayed on your profile page. + ' . esc_html($errors['profile_image']) . '

    '; ?> +
    +
    + + +
    +

    Training Organization Information

    +
    + + + ' . esc_html($errors['business_name']) . '

    '; ?> +
    +
    +
    + + + ' . esc_html($errors['business_phone']) . '

    '; ?> +
    +
    + + + ' . esc_html($errors['business_email']) . '

    '; ?> +
    +
    +
    + + + ' . esc_html($errors['business_website']) . '

    '; ?> +
    +
    + + + ' . esc_html($errors['business_description']) . '

    '; ?> +
    + + +
    + + + Please attach a .jpg, .png, or .gif image. This will be used as your organization's logo. + ' . esc_html($errors['org_logo']) . '

    '; ?> +
    + + +
    +

    Organization Headquarters

    +
    +
    +
    + + + ' . esc_html($errors['org_headquarters_city']) . '

    '; ?> +
    +
    + + + ' . esc_html($errors['org_headquarters_state']) . '

    '; ?> +
    +
    + + + ' . esc_html($errors['org_headquarters_country']) . '

    '; ?> +
    +
    + + +
    +

    Address Information

    +
    + + + ' . esc_html($errors['user_country']) . '

    '; ?> +
    + +
    +
    + + + + ' . esc_html($errors['user_state']) . '

    '; ?> + ' . esc_html($errors['user_state_other']) . '

    '; ?> +
    +
    + + + ' . esc_html($errors['user_city']) . '

    '; ?> +
    +
    + +
    + + + ' . esc_html($errors['user_zip']) . '

    '; ?> +
    + +
    +
    +
    + + What type of business are you? +
    + ' . esc_html($type) . ''; + } + ?> +
    + ' . esc_html($errors['business_type']) . '

    '; ?> +
    + +
    + + Who do you offer training to? (Select all that apply) +
    + "Anyone (open to the public)", + "Industry professionals" => "Industry professionals", + "Internal staff" => "Internal staff in my company", + "Registered students" => "Registered students/members of my org/institution" + ]; + $selected_audience = $data['training_audience'] ?? []; + if (!is_array($selected_audience)) $selected_audience = []; // Ensure it's an array + foreach ($audience_options as $value => $label) { + echo ''; + } + ?> +
    + ' . esc_html($errors['training_audience']) . '

    '; ?> +
    + +
    + + What formats of training do you offer? +
    + ' . esc_html($format) . ''; + } + ?> +
    + ' . esc_html($errors['training_formats']) . '

    '; ?> +
    + +
    + + Where are you willing to provide training? (Select all that apply) +
    + ' . esc_html($location) . ''; + } + ?> +
    + ' . esc_html($errors['training_locations']) . '

    '; ?> +
    + +
    + + What training resources do you have access to? (Select all that apply) +
    + ' . esc_html($resource) . ''; + } + ?> +
    + ' . esc_html($errors['training_resources']) . '

    '; ?> +
    +
    + + It's our goal to help you generate revenue through your training. How much revenue are you looking to generate annually though your training on Upskill HVAC? + +
    +
    + +
    + +
    + + + post_content, 'hvac_trainer_registration')) || + is_page('trainer/registration') || + is_page('trainer-registration') || + $current_path === 'trainer/registration' || + strpos($current_path, 'trainer/registration') !== false; + + if ($is_registration_page) { + wp_enqueue_style( + 'hvac-registration-style', + HVAC_PLUGIN_URL . 'assets/css/hvac-registration.css', // Ensure this CSS file exists and is styled + array(), + HVAC_PLUGIN_VERSION + ); + + wp_enqueue_script( + 'hvac-registration-js', + HVAC_PLUGIN_URL . 'assets/js/hvac-registration.js', // Ensure this JS file exists + array('jquery'), + HVAC_PLUGIN_VERSION, + true + ); + + // Localize script to pass states/provinces data and AJAX URL + wp_localize_script('hvac-registration-js', 'hvacRegistrationData', array( + 'ajax_url' => admin_url('admin-ajax.php'), // Needed if JS fetches states/provinces via AJAX + 'states' => $this->get_us_states(), // Pass US states + 'provinces' => $this->get_canadian_provinces(), // Pass CA provinces + // Pass other country data if needed, or handle via AJAX + )); + } + } + + /** + * Handle profile image upload after user is created. + * Should be called from within create_trainer_account or similar context. + * + * @param int $user_id The ID of the user to attach the image to. + * @param array $file_data The $_FILES array entry for the uploaded image. + * @return int|false Attachment ID on success, false on failure. + */ + private function handle_profile_image_upload($user_id, $file_data) { + // Basic validation already done in process_registration_submission + if (!$user_id || empty($file_data) || !isset($file_data['tmp_name']) || $file_data['error'] !== UPLOAD_ERR_OK) { + + return false; + } + + // These files need to be included as dependencies when on the front-end. + require_once(ABSPATH . 'wp-admin/includes/image.php'); + require_once(ABSPATH . 'wp-admin/includes/file.php'); + require_once(ABSPATH . 'wp-admin/includes/media.php'); + + // Let WordPress handle the upload. It moves the file and creates attachment post. + // Pass the $_FILES array key ('profile_image' in this case) + $attachment_id = media_handle_upload('profile_image', 0); // 0 means don't attach to a post + + if (is_wp_error($attachment_id)) { + // Handle upload error +// error_log('[HVAC REG DEBUG] Profile image upload error for user ' . $user_id . ': ' . $attachment_id->get_error_message()); + // Optionally add this error to be displayed to the user via transient? + // For now, just fail silently in terms of user feedback, but log it. + return false; + } else { + // Store the attachment ID as user meta + update_user_meta($user_id, 'profile_image_id', $attachment_id); + + return $attachment_id; + } + } + + /** + * Validate registration form data + * + * @param array $data Submitted form data ($_POST). + * @return array Array of errors, empty if valid. + */ + public function validate_registration($data) { +// error_log('[HVAC REG DEBUG] validate_registration: Received data: ' . print_r($data, true)); + $errors = array(); + + // Required field validation + $required_fields = [ + 'user_email' => 'Email', + 'user_pass' => 'Password', + 'confirm_password' => 'Confirm Password', + 'first_name' => 'First Name', + 'last_name' => 'Last Name', + 'display_name' => 'Display Name', + 'description' => 'Biographical Info', + 'business_name' => 'Business Name', + 'business_phone' => 'Business Phone', + 'business_email' => 'Business Email', + 'business_description' => 'Business Description', + 'user_country' => 'Country', + 'user_state' => 'State/Province', + 'user_city' => 'City', + 'user_zip' => 'Zip/Postal Code', + 'create_venue' => 'Create Training Venue Profile selection', + 'business_type' => 'Business Type', + 'application_details' => 'Application Details', + ]; + + foreach ($required_fields as $field => $label) { + // Use trim to catch spaces-only input + if (empty($data[$field]) || trim($data[$field]) === '') { + $errors[$field] = $label . ' is required.'; + } + } + + // Required checkbox groups + $required_checkboxes = [ + 'training_audience' => 'Training Audience', + 'training_formats' => 'Training Formats', + 'training_locations' => 'Training Locations', + 'training_resources' => 'Training Resources', + ]; + foreach ($required_checkboxes as $field => $label) { + // Check if the key exists and is a non-empty array + if (empty($data[$field]) || !is_array($data[$field])) { + $errors[$field] = 'Please select at least one option for ' . $label . '.'; + } + } + + // Email validation + if (!empty($data['user_email']) && !is_email($data['user_email'])) { + $errors['user_email'] = 'Please enter a valid email address.'; + } + if (!empty($data['business_email']) && !is_email($data['business_email'])) { + $errors['business_email'] = 'Please enter a valid business email address.'; + } + + // Email exists validation (only if email is valid) + if (empty($errors['user_email']) && !empty($data['user_email']) && email_exists($data['user_email'])) { + $errors['user_email'] = 'This email address is already registered.'; + } + + // Password validation + if (!empty($data['user_pass'])) { + if (strlen($data['user_pass']) < 8) { + $errors['user_pass'] = 'Password must be at least 8 characters long.'; + } elseif (!preg_match('/[A-Z]/', $data['user_pass'])) { + $errors['user_pass'] = 'Password must contain at least one uppercase letter.'; + } elseif (!preg_match('/[a-z]/', $data['user_pass'])) { + $errors['user_pass'] = 'Password must contain at least one lowercase letter.'; + } elseif (!preg_match('/[0-9]/', $data['user_pass'])) { + $errors['user_pass'] = 'Password must contain at least one number.'; + } + // Consider adding special character requirement if needed: !preg_match('/[\W_]/', $data['user_pass']) + } + + // Confirm password validation (only if password itself is not empty) + if (!empty($data['user_pass']) && empty($errors['user_pass'])) { + if (empty($data['confirm_password'])) { + $errors['confirm_password'] = 'Please confirm your password.'; + } elseif ($data['user_pass'] !== $data['confirm_password']) { + $errors['confirm_password'] = 'Passwords do not match.'; + } + } + + // URL validation (optional fields) + if (!empty($data['user_url']) && !filter_var($data['user_url'], FILTER_VALIDATE_URL)) { + $errors['user_url'] = 'Please enter a valid URL for your personal website.'; + } + if (!empty($data['user_linkedin']) && !filter_var($data['user_linkedin'], FILTER_VALIDATE_URL)) { + $errors['user_linkedin'] = 'Please enter a valid URL for your LinkedIn profile.'; + } + if (!empty($data['business_website']) && !filter_var($data['business_website'], FILTER_VALIDATE_URL)) { + $errors['business_website'] = 'Please enter a valid URL for your business website.'; + } + + // State/Province 'Other' validation + if (!empty($data['user_country'])) { + if ($data['user_country'] !== 'United States' && $data['user_country'] !== 'Canada') { + // If country is not US/CA, state *must* be 'Other' + if (empty($data['user_state']) || $data['user_state'] !== 'Other') { + $errors['user_state'] = 'Please select "Other" for State/Province if your country is not US or Canada.'; + } elseif (empty($data['user_state_other']) || trim($data['user_state_other']) === '') { + // If state is 'Other', the text input must not be empty + $errors['user_state_other'] = 'Please enter your state/province.'; + } + } elseif (!empty($data['user_state'])) { + // If country is US/CA + if ($data['user_state'] === 'Other') { + // State cannot be 'Other' if country is US/CA + $errors['user_state'] = 'Please select your state/province from the list.'; + } elseif (empty($errors['user_state'])) { // Only check 'Other' field if state itself is valid + // Ensure 'Other' text input is cleared if a valid state is selected + // This might be better handled by JS, but add server-side check just in case + if (!empty($data['user_state_other'])) { + // Maybe log a warning? Or just ignore it. Let's ignore for now. + + } + } + } + } + +// error_log('[HVAC REG DEBUG] validate_registration: FINAL errors before return: ' . print_r($errors, true)); + return $errors; + } + + /** + * Create trainer account and associated data + * + * @param array $data Sanitized form data. + * @param array|null $profile_image_data The $_FILES entry for the profile image, if provided. + * @return int|WP_Error User ID on success, WP_Error on failure. + */ + private function create_trainer_account($data, $profile_image_data = null) { + // Assume data is already somewhat validated by validate_registration + // Perform final sanitization here before insertion + $user_email = sanitize_email($data['user_email']); + $user_pass = $data['user_pass']; // wp_insert_user handles hashing + $first_name = sanitize_text_field($data['first_name']); + $last_name = sanitize_text_field($data['last_name']); + $display_name = sanitize_text_field($data['display_name']); + $user_url = !empty($data['user_url']) ? esc_url_raw($data['user_url']) : ''; + $description = wp_kses_post($data['description']); // Allow some HTML + + // Generate username from email (ensure uniqueness) + $username_base = sanitize_user(substr($user_email, 0, strpos($user_email, '@')), true); + if (empty($username_base)) { // Handle cases where email might be weird + $username_base = 'trainer'; + } + $username = $username_base; + $counter = 1; + while (username_exists($username)) { + $username = $username_base . $counter; + $counter++; + if ($counter > 100) { // Safety break + return new WP_Error('username_generation', 'Could not generate a unique username.'); + } + } + + // User data array + $user_data = array( + 'user_login' => $username, + 'user_email' => $user_email, + 'user_pass' => $user_pass, + 'first_name' => $first_name, + 'last_name' => $last_name, + 'display_name' => $display_name, + 'user_url' => $user_url, + 'description' => $description, + 'role' => 'hvac_trainer' // Assign custom role + ); + + // Insert the user + $user_id = wp_insert_user($user_data); + + // Check for errors + if (is_wp_error($user_id)) { +// error_log('[HVAC REG DEBUG] wp_insert_user failed: ' . $user_id->get_error_message()); + return $user_id; // Return the WP_Error object + } + + // --- Update User Meta --- + // Sanitize all meta values before updating + $meta_fields = [ + 'user_linkedin' => !empty($data['user_linkedin']) ? esc_url_raw($data['user_linkedin']) : '', + 'personal_accreditation' => !empty($data['personal_accreditation']) ? sanitize_text_field($data['personal_accreditation']) : '', + 'business_name' => sanitize_text_field($data['business_name']), + 'business_phone' => sanitize_text_field($data['business_phone']), + 'business_email' => sanitize_email($data['business_email']), + 'business_website' => !empty($data['business_website']) ? esc_url_raw($data['business_website']) : '', + 'business_description' => wp_kses_post($data['business_description']), + 'user_country' => sanitize_text_field($data['user_country']), + // Use the 'Other' field value if state was 'Other', otherwise use the selected state + 'user_state' => ($data['user_state'] === 'Other' && isset($data['user_state_other'])) ? sanitize_text_field($data['user_state_other']) : sanitize_text_field($data['user_state']), + 'user_city' => sanitize_text_field($data['user_city']), + 'user_zip' => sanitize_text_field($data['user_zip']), + 'create_venue' => sanitize_text_field($data['create_venue']), // Should be 'Yes' or 'No' + 'business_type' => sanitize_text_field($data['business_type']), + 'training_audience' => (!empty($data['training_audience']) && is_array($data['training_audience'])) ? array_map('sanitize_text_field', $data['training_audience']) : [], + 'training_formats' => (!empty($data['training_formats']) && is_array($data['training_formats'])) ? array_map('sanitize_text_field', $data['training_formats']) : [], + 'training_locations' => (!empty($data['training_locations']) && is_array($data['training_locations'])) ? array_map('sanitize_text_field', $data['training_locations']) : [], + 'training_resources' => (!empty($data['training_resources']) && is_array($data['training_resources'])) ? array_map('sanitize_text_field', $data['training_resources']) : [], + 'application_details' => wp_kses_post($data['application_details']), + 'annual_revenue_target' => !empty($data['annual_revenue_target']) ? intval($data['annual_revenue_target']) : '', + 'account_status' => 'pending' // Set initial status + ]; + + foreach ($meta_fields as $key => $value) { + update_user_meta($user_id, $key, $value); + } + + // --- Handle Profile Image Upload --- + // Note: handle_profile_image_upload uses media_handle_upload which expects the key from $_FILES + if ($profile_image_data) { + + // We don't need the return value here unless we want to report specific upload errors + $this->handle_profile_image_upload($user_id, $profile_image_data); // Pass the $_FILES entry + } + + // --- Create Organizer Profile --- + + $organizer_id = $this->create_organizer_profile($user_id, $meta_fields); // Pass sanitized meta fields + if ($organizer_id) { + + update_user_meta($user_id, 'hvac_organizer_id', $organizer_id); + } else { + + // Consider returning an error if this is critical + // return new WP_Error('organizer_creation', 'Failed to create the associated organizer profile.'); + } + + // --- Create Training Venue (if requested) --- + if (isset($meta_fields['create_venue']) && $meta_fields['create_venue'] === 'Yes') { + + $venue_id = $this->create_training_venue($user_id, $meta_fields); // Pass sanitized meta fields + if ($venue_id) { + + update_user_meta($user_id, 'hvac_venue_id', $venue_id); + } else { + + // Consider returning an error if this is critical + // return new WP_Error('venue_creation', 'Failed to create the associated training venue.'); + } + } + + // --- Set Account Status to Pending --- + // This is already done via user meta, but could also involve custom capabilities or flags + // update_user_meta($user_id, 'account_status', 'pending'); // Redundant if set above + + return $user_id; // Return user ID on success + } + + /** + * Create or update an Organizer profile linked to the user using sanitized data. + * + * @param int $user_id The user ID. + * @param array $meta_data Array of sanitized user meta data. + * @return int|false Organizer Post ID on success, false on failure. + */ + private function create_organizer_profile($user_id, $meta_data) { + if (!class_exists('Tribe__Events__Main') || !function_exists('tribe_create_organizer')) { + + return false; + } + + $organizer_data = array( + 'Organizer' => $meta_data['business_name'], // Use sanitized business name + 'Phone' => $meta_data['business_phone'], + 'Website' => $meta_data['business_website'], + 'Email' => $meta_data['business_email'], + 'Description' => $meta_data['business_description'], + 'post_status' => 'publish', // Publish organizer immediately + 'post_author' => $user_id // Associate with the new user + ); + + // Check if an organizer already exists for this user + $existing_organizer_id = get_user_meta($user_id, 'hvac_organizer_id', true); + + if ($existing_organizer_id && get_post_type($existing_organizer_id) === Tribe__Events__Main::ORGANIZER_POST_TYPE) { + // Update existing organizer + $organizer_data['ID'] = $existing_organizer_id; + $organizer_id = tribe_update_organizer($existing_organizer_id, $organizer_data); + + } else { + // Create new organizer + $organizer_id = tribe_create_organizer($organizer_data); + + } + + if (is_wp_error($organizer_id)) { +// error_log('[HVAC REG DEBUG] Error creating/updating organizer: ' . $organizer_id->get_error_message()); + return false; + } elseif (!$organizer_id || $organizer_id === 0) { // Check for 0 as well + + return false; + } + + return (int) $organizer_id; + } + + /** + * Create or update a Venue profile linked to the user using sanitized data. + * + * @param int $user_id The user ID. + * @param array $meta_data Array of sanitized user meta data. + * @return int|false Venue Post ID on success, false on failure. + */ + private function create_training_venue($user_id, $meta_data) { + if (!class_exists('Tribe__Events__Main') || !function_exists('tribe_create_venue')) { + + return false; + } + + // Use the already processed state/province from meta + $state_province = $meta_data['user_state']; + + $venue_data = array( + 'Venue' => $meta_data['business_name'] . ' Training Venue', // Venue name from sanitized meta + 'Country' => $meta_data['user_country'], + 'Address' => '', // TEC doesn't have a single address line, use City/State/Zip + 'City' => $meta_data['user_city'], + 'StateProvince' => $state_province, + 'State' => $state_province, // Also set State field + 'Province' => $state_province, // Also set Province field + 'Zip' => $meta_data['user_zip'], + 'Phone' => $meta_data['business_phone'], + 'Website' => $meta_data['business_website'], + 'post_status' => 'publish', // Publish venue immediately + 'post_author' => $user_id // Associate with the new user + ); + + // Check if a venue already exists for this user + $existing_venue_id = get_user_meta($user_id, 'hvac_venue_id', true); + + if ($existing_venue_id && get_post_type($existing_venue_id) === Tribe__Events__Main::VENUE_POST_TYPE) { + // Update existing venue + $venue_data['ID'] = $existing_venue_id; + $venue_id = tribe_update_venue($existing_venue_id, $venue_data); + + } else { + // Create new venue + $venue_id = tribe_create_venue($venue_data); + + } + + if (is_wp_error($venue_id)) { +// error_log('[HVAC REG DEBUG] Error creating/updating venue: ' . $venue_id->get_error_message()); + return false; + } elseif (!$venue_id || $venue_id === 0) { // Check for 0 as well + + return false; + } + + return (int) $venue_id; + } + + /** + * Send notification email to admin about new registration + * + * @param int $user_id The ID of the newly registered user. + * @param array $data The raw submitted form data (used for notification content). + */ + private function send_admin_notification($user_id, $data) { + $admin_email = get_option('admin_email'); + if (!$admin_email) { + + return; + } + + $user_info = get_userdata($user_id); + if (!$user_info) { + + return; + } + + $subject = sprintf('%s - New HVAC Trainer Registration Pending Approval', get_bloginfo('name')); + + // Use sanitized data for the email body where appropriate + $message = "A new HVAC trainer has registered and is awaiting approval.\n\n"; + $message .= "Details:\n"; + $message .= "Username: " . $user_info->user_login . "\n"; + $message .= "Email: " . $user_info->user_email . "\n"; + // Use the sanitized first/last name from user_info if available, fallback to data + $first_name = $user_info->first_name ?: sanitize_text_field($data['first_name']); + $last_name = $user_info->last_name ?: sanitize_text_field($data['last_name']); + $message .= "Name: " . $first_name . " " . $last_name . "\n"; + $message .= "Business Name: " . sanitize_text_field($data['business_name']) . "\n"; // Use raw data as meta might not be fully updated yet? Safer to use raw. + $message .= "Application Details:\n" . wp_kses_post($data['application_details']) . "\n\n"; // Use wp_kses_post for safety + + // Add link to user profile in admin + $profile_link = admin_url('user-edit.php?user_id=' . $user_id); + $message .= "Approve/Deny User: " . $profile_link . "\n"; + + $headers = array('Content-Type: text/plain; charset=UTF-8'); + + if (wp_mail($admin_email, $subject, $message, $headers)) { + + } else { + + // Consider adding an error to the transient if email failure is critical? + // $errors['notification'] = 'Admin notification failed. Registration complete but please contact support.'; + } + } + + // TODO: Add send_user_pending_notification() + // TODO: Add send_user_approved_notification() + // TODO: Add send_user_denied_notification() + + /** + * Renders the edit profile form shortcode output + */ + public function render_edit_profile_form() { + if (!is_user_logged_in()) { + return '

    Please log in to edit your profile.

    '; + } + + // We don't need to do anything here since the template file already handles the form rendering + // Just set up the shortcode to point to the template + ob_start(); + include HVAC_PLUGIN_DIR . 'templates/template-edit-profile.php'; + return ob_get_clean(); + } + + /** + * Process profile update submission from the edit profile form + */ + public function process_profile_update() { + // Only logged-in users can update profiles + if (!is_user_logged_in()) { + wp_redirect(home_url('/community-login/')); + exit; + } + + $user_id = get_current_user_id(); + $user = get_userdata($user_id); + $errors = []; + $submitted_data = $_POST; + $profile_page_url = home_url('/edit-profile/'); + + // Verify nonce + if (!isset($_POST['hvac_profile_nonce']) || !wp_verify_nonce($_POST['hvac_profile_nonce'], 'hvac_update_profile')) { + $errors['nonce'] = 'Security check failed. Please try submitting the form again.'; + + $this->redirect_with_profile_errors($errors, $profile_page_url); + // No need for return/exit here, redirect_with_errors exits. + } + + // File Upload Handling + $profile_image_data = null; + if (isset($_FILES['profile_image']) && $_FILES['profile_image']['error'] !== UPLOAD_ERR_NO_FILE) { + if ($_FILES['profile_image']['error'] === UPLOAD_ERR_OK) { + // Check if it's actually an uploaded file + if (!is_uploaded_file($_FILES['profile_image']['tmp_name'])) { + $errors['profile_image'] = 'File upload error (invalid temp file).'; + + } else { + $allowed_types = ['image/jpeg', 'image/png', 'image/gif']; + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime_type = finfo_file($finfo, $_FILES['profile_image']['tmp_name']); + finfo_close($finfo); + + if (!in_array($mime_type, $allowed_types)) { + $errors['profile_image'] = 'Invalid file type detected (' . esc_html($mime_type) . '). Please upload a JPG, PNG, or GIF.'; + + } else { + $profile_image_data = $_FILES['profile_image']; // Store the whole $_FILES entry + + } + } + } else { + $errors['profile_image'] = 'There was an error uploading the profile image. Code: ' . $_FILES['profile_image']['error']; + + } + } + + // Validate form data + $validation_errors = $this->validate_profile_update($submitted_data, $user); + $errors = array_merge($errors, $validation_errors); + + // Process if no errors + if (empty($errors)) { + + $update_success = $this->update_user_profile($user_id, $submitted_data, $profile_image_data); + + if (is_wp_error($update_success)) { + $errors['account'] = $update_success->get_error_message(); +// error_log('[HVAC PROFILE DEBUG] Profile update WP_Error: ' . $update_success->get_error_message()); + $this->redirect_with_profile_errors($errors, $profile_page_url); + } elseif ($update_success) { + + // Redirect to the profile page with success message + wp_safe_redirect(add_query_arg('updated', '1', $profile_page_url)); + exit; + } else { + $errors['account'] = 'An unknown error occurred during profile update. Please try again.'; + + $this->redirect_with_profile_errors($errors, $profile_page_url); + } + } else { +// error_log('[HVAC PROFILE DEBUG] Validation errors found in profile update: ' . print_r($errors, true)); + $this->redirect_with_profile_errors($errors, $profile_page_url); + } + } + + /** + * Helper function to store profile errors in transient and redirect back to the form page. + * + * @param array $errors Array of error messages. + * @param string $redirect_url The URL to redirect back to. + */ + private function redirect_with_profile_errors($errors, $redirect_url) { + $transient_id = uniqid(); // Generate unique ID for transient key + $transient_key = self::PROFILE_TRANSIENT_PREFIX . $transient_id; + $transient_data = [ + 'errors' => $errors, + ]; + // Store for 5 minutes + set_transient($transient_key, $transient_data, MINUTE_IN_SECONDS * 5); + + // Add query arguments to the redirect URL + $redirect_url = add_query_arg([ + 'prof_error' => '1', + 'tid' => $transient_id, + ], $redirect_url); + + wp_safe_redirect($redirect_url); + exit; // Stop execution after redirect + } + + /** + * Validate profile update data + * + * @param array $data Submitted form data ($_POST). + * @param WP_User $user Current user object. + * @return array Array of errors, empty if valid. + */ + public function validate_profile_update($data, $user) { + + $errors = array(); + + // Required field validation + $required_fields = [ + 'first_name' => 'First Name', + 'last_name' => 'Last Name', + 'display_name' => 'Display Name', + 'user_email' => 'Email', + 'description' => 'Biographical Info', + 'business_name' => 'Business Name', + 'business_phone' => 'Business Phone', + 'business_email' => 'Business Email', + 'business_description' => 'Business Description', + 'user_country' => 'Country', + 'user_state' => 'State/Province', + 'user_city' => 'City', + 'user_zip' => 'Zip/Postal Code', + 'business_type' => 'Business Type', + ]; + + foreach ($required_fields as $field => $label) { + // Use trim to catch spaces-only input + if (empty($data[$field]) || trim($data[$field]) === '') { + $errors[$field] = $label . ' is required.'; + } + } + + // Required checkbox groups + $required_checkboxes = [ + 'training_audience' => 'Training Audience', + 'training_formats' => 'Training Formats', + 'training_locations' => 'Training Locations', + 'training_resources' => 'Training Resources', + ]; + foreach ($required_checkboxes as $field => $label) { + // Check if the key exists and is a non-empty array + if (empty($data[$field]) || !is_array($data[$field])) { + $errors[$field] = 'Please select at least one option for ' . $label . '.'; + } + } + + // Email validation + if (!empty($data['user_email']) && !is_email($data['user_email'])) { + $errors['user_email'] = 'Please enter a valid email address.'; + } + if (!empty($data['business_email']) && !is_email($data['business_email'])) { + $errors['business_email'] = 'Please enter a valid business email address.'; + } + + // Email exists validation (only if email is changed and valid) + if (empty($errors['user_email']) && !empty($data['user_email']) && $data['user_email'] !== $user->user_email && email_exists($data['user_email'])) { + $errors['user_email'] = 'This email address is already registered to another account.'; + } + + // URL validation (optional fields) + if (!empty($data['user_url']) && !filter_var($data['user_url'], FILTER_VALIDATE_URL)) { + $errors['user_url'] = 'Please enter a valid URL for your personal website.'; + } + if (!empty($data['user_linkedin']) && !filter_var($data['user_linkedin'], FILTER_VALIDATE_URL)) { + $errors['user_linkedin'] = 'Please enter a valid URL for your LinkedIn profile.'; + } + if (!empty($data['business_website']) && !filter_var($data['business_website'], FILTER_VALIDATE_URL)) { + $errors['business_website'] = 'Please enter a valid URL for your business website.'; + } + + // State/Province 'Other' validation + if (!empty($data['user_country'])) { + if ($data['user_country'] !== 'United States' && $data['user_country'] !== 'Canada') { + // If country is not US/CA, state *must* be 'Other' + if (empty($data['user_state']) || $data['user_state'] !== 'Other') { + $errors['user_state'] = 'Please select "Other" for State/Province if your country is not US or Canada.'; + } elseif (empty($data['user_state_other']) || trim($data['user_state_other']) === '') { + // If state is 'Other', the text input must not be empty + $errors['user_state_other'] = 'Please enter your state/province.'; + } + } elseif (!empty($data['user_state'])) { + // If country is US/CA + if ($data['user_state'] === 'Other') { + // State cannot be 'Other' if country is US/CA + $errors['user_state'] = 'Please select your state/province from the list.'; + } + } + } + + // Password validation (only if user is attempting to change password) + if (!empty($data['current_password']) || !empty($data['new_password']) || !empty($data['confirm_new_password'])) { + // Verify current password + if (empty($data['current_password'])) { + $errors['current_password'] = 'Please enter your current password.'; + } elseif (!wp_check_password($data['current_password'], $user->user_pass, $user->ID)) { + $errors['current_password'] = 'Current password is incorrect.'; + } + + // Validate new password + if (empty($data['new_password'])) { + $errors['new_password'] = 'Please enter a new password.'; + } elseif (strlen($data['new_password']) < 8) { + $errors['new_password'] = 'Password must be at least 8 characters long.'; + } elseif (!preg_match('/[A-Z]/', $data['new_password'])) { + $errors['new_password'] = 'Password must contain at least one uppercase letter.'; + } elseif (!preg_match('/[a-z]/', $data['new_password'])) { + $errors['new_password'] = 'Password must contain at least one lowercase letter.'; + } elseif (!preg_match('/[0-9]/', $data['new_password'])) { + $errors['new_password'] = 'Password must contain at least one number.'; + } + + // Confirm new password + if (empty($data['confirm_new_password'])) { + $errors['confirm_new_password'] = 'Please confirm your new password.'; + } elseif ($data['new_password'] !== $data['confirm_new_password']) { + $errors['confirm_new_password'] = 'New passwords do not match.'; + } + } + +// error_log('[HVAC PROFILE DEBUG] validate_profile_update: Validation result - ' . (empty($errors) ? 'VALID' : 'INVALID')); + return $errors; + } + + /** + * Update the user profile with submitted data + * + * @param int $user_id The user ID to update. + * @param array $data The submitted form data. + * @param array|null $profile_image_data The profile image file data, if any. + * @return bool|WP_Error True on success, WP_Error on failure. + */ + private function update_user_profile($user_id, $data, $profile_image_data = null) { + // Sanitize and prepare user data for update + $userdata = array( + 'ID' => $user_id, + 'first_name' => sanitize_text_field($data['first_name']), + 'last_name' => sanitize_text_field($data['last_name']), + 'display_name' => sanitize_text_field($data['display_name']), + 'user_email' => sanitize_email($data['user_email']), + 'user_url' => !empty($data['user_url']) ? esc_url_raw($data['user_url']) : '', + 'description' => wp_kses_post($data['description']), + ); + + // Handle password update if provided + if (!empty($data['new_password']) && !empty($data['current_password']) && !empty($data['confirm_new_password'])) { + $userdata['user_pass'] = $data['new_password']; // wp_update_user will hash the password + } + + // Update user data + $update_result = wp_update_user($userdata); + + if (is_wp_error($update_result)) { +// error_log('[HVAC PROFILE DEBUG] wp_update_user failed: ' . $update_result->get_error_message()); + return $update_result; + } + + // Update user meta + $meta_fields = [ + 'user_linkedin' => !empty($data['user_linkedin']) ? esc_url_raw($data['user_linkedin']) : '', + 'personal_accreditation' => !empty($data['personal_accreditation']) ? sanitize_text_field($data['personal_accreditation']) : '', + 'business_name' => sanitize_text_field($data['business_name']), + 'business_phone' => sanitize_text_field($data['business_phone']), + 'business_email' => sanitize_email($data['business_email']), + 'business_website' => !empty($data['business_website']) ? esc_url_raw($data['business_website']) : '', + 'business_description' => wp_kses_post($data['business_description']), + 'user_country' => sanitize_text_field($data['user_country']), + 'user_state' => ($data['user_state'] === 'Other' && isset($data['user_state_other'])) ? sanitize_text_field($data['user_state_other']) : sanitize_text_field($data['user_state']), + 'user_city' => sanitize_text_field($data['user_city']), + 'user_zip' => sanitize_text_field($data['user_zip']), + 'business_type' => sanitize_text_field($data['business_type']), + 'training_audience' => (!empty($data['training_audience']) && is_array($data['training_audience'])) ? array_map('sanitize_text_field', $data['training_audience']) : [], + 'training_formats' => (!empty($data['training_formats']) && is_array($data['training_formats'])) ? array_map('sanitize_text_field', $data['training_formats']) : [], + 'training_locations' => (!empty($data['training_locations']) && is_array($data['training_locations'])) ? array_map('sanitize_text_field', $data['training_locations']) : [], + 'training_resources' => (!empty($data['training_resources']) && is_array($data['training_resources'])) ? array_map('sanitize_text_field', $data['training_resources']) : [], + 'annual_revenue_target' => !empty($data['annual_revenue_target']) ? intval($data['annual_revenue_target']) : '', + ]; + + foreach ($meta_fields as $key => $value) { + update_user_meta($user_id, $key, $value); + } + + // Handle profile image upload if provided + if ($profile_image_data) { + + // We don't need the return value here unless we want to report specific upload errors + $this->handle_profile_image_upload($user_id, $profile_image_data); + } + + // Update organizer profile if it exists + $organizer_id = get_user_meta($user_id, 'hvac_organizer_id', true); + if ($organizer_id && get_post_type($organizer_id) === Tribe__Events__Main::ORGANIZER_POST_TYPE) { + + $this->create_organizer_profile($user_id, $meta_fields); + } + + // Update venue profile if it exists + $venue_id = get_user_meta($user_id, 'hvac_venue_id', true); + if ($venue_id && get_post_type($venue_id) === Tribe__Events__Main::VENUE_POST_TYPE) { + + $this->create_training_venue($user_id, $meta_fields); + } + + return true; + } + + /** + * Get list of countries (simplified) + */ + private function get_country_list() { + // In a real application, use a more comprehensive list or library + return array( + 'US' => 'United States', + 'CA' => 'Canada', + // Add more countries as needed + 'GB' => 'United Kingdom', + 'AU' => 'Australia', + // ... + ); + } + /** + * Get list of US states + */ + private function get_us_states() { + // Use state abbreviations as keys if preferred by JS/validation + return array( + 'AL' => 'Alabama', 'AK' => 'Alaska', 'AZ' => 'Arizona', 'AR' => 'Arkansas', 'CA' => 'California', + 'CO' => 'Colorado', 'CT' => 'Connecticut', 'DE' => 'Delaware', 'DC' => 'District of Columbia', 'FL' => 'Florida', + 'GA' => 'Georgia', 'HI' => 'Hawaii', 'ID' => 'Idaho', 'IL' => 'Illinois', 'IN' => 'Indiana', + 'IA' => 'Iowa', 'KS' => 'Kansas', 'KY' => 'Kentucky', 'LA' => 'Louisiana', 'ME' => 'Maine', + 'MD' => 'Maryland', 'MA' => 'Massachusetts', 'MI' => 'Michigan', 'MN' => 'Minnesota', 'MS' => 'Mississippi', + 'MO' => 'Missouri', 'MT' => 'Montana', 'NE' => 'Nebraska', 'NV' => 'Nevada', 'NH' => 'New Hampshire', + 'NJ' => 'New Jersey', 'NM' => 'New Mexico', 'NY' => 'New York', 'NC' => 'North Carolina', 'ND' => 'North Dakota', + 'OH' => 'Ohio', 'OK' => 'Oklahoma', 'OR' => 'Oregon', 'PA' => 'Pennsylvania', 'RI' => 'Rhode Island', + 'SC' => 'South Carolina', 'SD' => 'South Dakota', 'TN' => 'Tennessee', 'TX' => 'Texas', 'UT' => 'Utah', + 'VT' => 'Vermont', 'VA' => 'Virginia', 'WA' => 'Washington', 'WV' => 'West Virginia', 'WI' => 'Wisconsin', + 'WY' => 'Wyoming' + ); + } + + /** + * Get list of Canadian provinces + */ + private function get_canadian_provinces() { + // Use province abbreviations as keys if preferred by JS/validation + return array( + 'AB' => 'Alberta', 'BC' => 'British Columbia', 'MB' => 'Manitoba', 'NB' => 'New Brunswick', + 'NL' => 'Newfoundland and Labrador', 'NS' => 'Nova Scotia', 'ON' => 'Ontario', 'PE' => 'Prince Edward Island', + 'QC' => 'Quebec', 'SK' => 'Saskatchewan', 'NT' => 'Northwest Territories', 'NU' => 'Nunavut', 'YT' => 'Yukon' + ); + } + +} // End class HVAC_Registration \ No newline at end of file diff --git a/includes/class-hvac-registration.php b/includes/class-hvac-registration.php index 7525e7db..eb3e073f 100644 --- a/includes/class-hvac-registration.php +++ b/includes/class-hvac-registration.php @@ -136,6 +136,29 @@ class HVAC_Registration { // error_log('[HVAC REG DEBUG] process_registration_submission: Checking if errors is empty. Result: ' . (empty($errors) ? 'Yes' : 'No')); } } + + // Handle Organization Logo Upload + $org_logo_data = null; + if (isset($_FILES['org_logo']) && $_FILES['org_logo']['error'] !== UPLOAD_ERR_NO_FILE) { + if ($_FILES['org_logo']['error'] === UPLOAD_ERR_OK) { + if (!is_uploaded_file($_FILES['org_logo']['tmp_name'])) { + $errors['org_logo'] = 'File upload error (invalid temp file).'; + } else { + $allowed_types = ['image/jpeg', 'image/png', 'image/gif']; + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime_type = finfo_file($finfo, $_FILES['org_logo']['tmp_name']); + finfo_close($finfo); + + if (!in_array($mime_type, $allowed_types)) { + $errors['org_logo'] = 'Invalid file type detected (' . esc_html($mime_type) . '). Please upload a JPG, PNG, or GIF.'; + } else { + $org_logo_data = $_FILES['org_logo']; + } + } + } else { + $errors['org_logo'] = 'There was an error uploading the organization logo. Code: ' . $_FILES['org_logo']['error']; + } + } // --- End File Upload Handling --- // Validate the rest of the form data @@ -145,7 +168,7 @@ class HVAC_Registration { // --- Process if No Errors --- if (empty($errors)) { - $user_id = $this->create_trainer_account($submitted_data, $profile_image_data); + $user_id = $this->create_trainer_account($submitted_data, $profile_image_data, $org_logo_data); if (is_wp_error($user_id)) { $errors['account'] = $user_id->get_error_message(); @@ -311,6 +334,12 @@ class HVAC_Registration { A short bio about yourself. This will be displayed on your profile page. ' . esc_html($errors['description']) . '

    '; ?> +
    + + Please explain why you want to create a training account on Upskill HVAC. + + ' . esc_html($errors['application_details']) . '

    '; ?> +
    @@ -319,119 +348,71 @@ class HVAC_Registration {
    - +
    -

    Business Information

    +

    Training Organization Information

    - + ' . esc_html($errors['business_name']) . '

    '; ?>
    - + ' . esc_html($errors['business_phone']) . '

    '; ?>
    - + ' . esc_html($errors['business_email']) . '

    '; ?>
    - + ' . esc_html($errors['business_website']) . '

    '; ?>
    - + ' . esc_html($errors['business_description']) . '

    '; ?>
    -
    - - -
    -

    Address Information

    -
    - - - ' . esc_html($errors['user_country']) . '

    '; ?> -
    - -
    -
    - - - - ' . esc_html($errors['user_state']) . '

    '; ?> - ' . esc_html($errors['user_state_other']) . '

    '; ?> -
    -
    - - - ' . esc_html($errors['user_city']) . '

    '; ?> -
    -
    - -
    - - - ' . esc_html($errors['user_zip']) . '

    '; ?> -
    -
    - - -
    -

    Training Venue

    + +
    - - Do you want to create a Training Venue Profile for your business to use when listing your training events? If yes, we will use the address provided above. -
    - - -
    - ' . esc_html($errors['create_venue']) . '

    '; ?> + + + Please attach a .jpg, .png, or .gif image. This will be used as your organization's logo. + ' . esc_html($errors['org_logo']) . '

    '; ?> +
    + + +
    +

    Organization Headquarters

    +
    +
    +
    + + + ' . esc_html($errors['org_headquarters_city']) . '

    '; ?> +
    +
    + + + ' . esc_html($errors['org_headquarters_state']) . '

    '; ?> +
    +
    + + + ' . esc_html($errors['org_headquarters_country']) . '

    '; ?> +
    +
    + + +
    +

    Training Capabilities

    -
    - - -
    -

    Training Information

    What type of business are you? @@ -514,23 +495,113 @@ class HVAC_Registration {
    ' . esc_html($errors['training_resources']) . '

    '; ?>
    - - - -
    -

    Application Details

    -
    - - Please explain why you want to create a training account on Upskill HVAC. - - ' . esc_html($errors['application_details']) . '

    '; ?> -
    It's our goal to help you generate revenue through your training. How much revenue are you looking to generate annually though your training on Upskill HVAC?
    -
    + + + +
    +

    Training Venue Information

    +
    + + Do you want to create a Training Venue Profile for your organization? This will be used when listing your training events. +
    + + +
    + ' . esc_html($errors['create_venue']) . '

    '; ?> +
    + +
    +
    + + + Defaults to "[Organization Name] of [City]" + ' . esc_html($errors['venue_name']) . '

    '; ?> +
    +
    + + + ' . esc_html($errors['venue_address']) . '

    '; ?> +
    +
    + + + ' . esc_html($errors['user_country']) . '

    '; ?> +
    + +
    +
    + + + + ' . esc_html($errors['user_state']) . '

    '; ?> + ' . esc_html($errors['user_state_other']) . '

    '; ?> +
    +
    + + + ' . esc_html($errors['user_city']) . '

    '; ?> +
    +
    + +
    + + + ' . esc_html($errors['user_zip']) . '

    '; ?> +
    + +
    +
    + + + ' . esc_html($errors['venue_phone']) . '

    '; ?> +
    +
    + + + ' . esc_html($errors['venue_website']) . '

    '; ?> +
    +
    + +
    +
    @@ -616,6 +687,36 @@ class HVAC_Registration { return $attachment_id; } } + + /** + * Handle organization logo upload + * + * @param int $organizer_id The ID of the organizer post to attach the image to. + * @param array $file_data The $_FILES array entry for the uploaded image. + * @return int|false Attachment ID on success, false on failure. + */ + private function handle_org_logo_upload($organizer_id, $file_data) { + if (!$organizer_id || empty($file_data) || !isset($file_data['tmp_name']) || $file_data['error'] !== UPLOAD_ERR_OK) { + return false; + } + + require_once(ABSPATH . 'wp-admin/includes/image.php'); + require_once(ABSPATH . 'wp-admin/includes/file.php'); + require_once(ABSPATH . 'wp-admin/includes/media.php'); + + // Set up the file for upload - need to temporarily set $_FILES for media_handle_upload + $_FILES['org_logo_temp'] = $file_data; + $attachment_id = media_handle_upload('org_logo_temp', $organizer_id); + unset($_FILES['org_logo_temp']); + + if (is_wp_error($attachment_id)) { + return false; + } else { + // Set as featured image for the organizer + set_post_thumbnail($organizer_id, $attachment_id); + return $attachment_id; + } + } /** * Validate registration form data @@ -636,25 +737,48 @@ class HVAC_Registration { 'last_name' => 'Last Name', 'display_name' => 'Display Name', 'description' => 'Biographical Info', - 'business_name' => 'Business Name', - 'business_phone' => 'Business Phone', - 'business_email' => 'Business Email', - 'business_description' => 'Business Description', - 'user_country' => 'Country', - 'user_state' => 'State/Province', - 'user_city' => 'City', - 'user_zip' => 'Zip/Postal Code', + 'business_name' => 'Organization Name', + 'business_phone' => 'Organization Phone', + 'business_email' => 'Organization Email', + 'business_description' => 'Organization Description', + 'org_logo' => 'Organization Logo', 'create_venue' => 'Create Training Venue Profile selection', 'business_type' => 'Business Type', 'application_details' => 'Application Details', ]; foreach ($required_fields as $field => $label) { + // Special handling for file upload + if ($field === 'org_logo') { + if (!isset($_FILES['org_logo']) || $_FILES['org_logo']['error'] === UPLOAD_ERR_NO_FILE) { + $errors[$field] = $label . ' is required.'; + } + continue; + } + // Use trim to catch spaces-only input if (empty($data[$field]) || trim($data[$field]) === '') { $errors[$field] = $label . ' is required.'; } } + + // Conditional venue fields validation + if (isset($data['create_venue']) && $data['create_venue'] === 'Yes') { + $venue_required_fields = [ + 'venue_name' => 'Venue Name', + 'venue_address' => 'Street Address', + 'user_country' => 'Country', + 'user_state' => 'State/Province', + 'user_city' => 'City', + 'user_zip' => 'Zip/Postal Code', + ]; + + foreach ($venue_required_fields as $field => $label) { + if (empty($data[$field]) || trim($data[$field]) === '') { + $errors[$field] = $label . ' is required for venue creation.'; + } + } + } // Required checkbox groups $required_checkboxes = [ @@ -718,7 +842,7 @@ class HVAC_Registration { } // State/Province 'Other' validation - if (!empty($data['user_country'])) { + if (!empty($data['user_country']) && isset($data['create_venue']) && $data['create_venue'] === 'Yes') { if ($data['user_country'] !== 'United States' && $data['user_country'] !== 'Canada') { // If country is not US/CA, state *must* be 'Other' if (empty($data['user_state']) || $data['user_state'] !== 'Other') { @@ -752,9 +876,10 @@ class HVAC_Registration { * * @param array $data Sanitized form data. * @param array|null $profile_image_data The $_FILES entry for the profile image, if provided. + * @param array|null $org_logo_data The $_FILES entry for the organization logo, if provided. * @return int|WP_Error User ID on success, WP_Error on failure. */ - private function create_trainer_account($data, $profile_image_data = null) { + private function create_trainer_account($data, $profile_image_data = null, $org_logo_data = null) { // Assume data is already somewhat validated by validate_registration // Perform final sanitization here before insertion $user_email = sanitize_email($data['user_email']); @@ -812,11 +937,9 @@ class HVAC_Registration { 'business_email' => sanitize_email($data['business_email']), 'business_website' => !empty($data['business_website']) ? esc_url_raw($data['business_website']) : '', 'business_description' => wp_kses_post($data['business_description']), - 'user_country' => sanitize_text_field($data['user_country']), - // Use the 'Other' field value if state was 'Other', otherwise use the selected state - 'user_state' => ($data['user_state'] === 'Other' && isset($data['user_state_other'])) ? sanitize_text_field($data['user_state_other']) : sanitize_text_field($data['user_state']), - 'user_city' => sanitize_text_field($data['user_city']), - 'user_zip' => sanitize_text_field($data['user_zip']), + 'org_headquarters_city' => !empty($data['org_headquarters_city']) ? sanitize_text_field($data['org_headquarters_city']) : '', + 'org_headquarters_state' => !empty($data['org_headquarters_state']) ? sanitize_text_field($data['org_headquarters_state']) : '', + 'org_headquarters_country' => !empty($data['org_headquarters_country']) ? sanitize_text_field($data['org_headquarters_country']) : '', 'create_venue' => sanitize_text_field($data['create_venue']), // Should be 'Yes' or 'No' 'business_type' => sanitize_text_field($data['business_type']), 'training_audience' => (!empty($data['training_audience']) && is_array($data['training_audience'])) ? array_map('sanitize_text_field', $data['training_audience']) : [], @@ -827,6 +950,19 @@ class HVAC_Registration { 'annual_revenue_target' => !empty($data['annual_revenue_target']) ? intval($data['annual_revenue_target']) : '', 'account_status' => 'pending' // Set initial status ]; + + // If venue creation is requested, store venue-specific data + if (isset($data['create_venue']) && $data['create_venue'] === 'Yes') { + $meta_fields['venue_name'] = !empty($data['venue_name']) ? sanitize_text_field($data['venue_name']) : ''; + $meta_fields['venue_address'] = !empty($data['venue_address']) ? sanitize_text_field($data['venue_address']) : ''; + $meta_fields['user_country'] = sanitize_text_field($data['user_country']); + // Use the 'Other' field value if state was 'Other', otherwise use the selected state + $meta_fields['user_state'] = ($data['user_state'] === 'Other' && isset($data['user_state_other'])) ? sanitize_text_field($data['user_state_other']) : sanitize_text_field($data['user_state']); + $meta_fields['user_city'] = sanitize_text_field($data['user_city']); + $meta_fields['user_zip'] = sanitize_text_field($data['user_zip']); + $meta_fields['venue_phone'] = !empty($data['venue_phone']) ? sanitize_text_field($data['venue_phone']) : ''; + $meta_fields['venue_website'] = !empty($data['venue_website']) ? esc_url_raw($data['venue_website']) : ''; + } foreach ($meta_fields as $key => $value) { update_user_meta($user_id, $key, $value); @@ -842,7 +978,7 @@ class HVAC_Registration { // --- Create Organizer Profile --- - $organizer_id = $this->create_organizer_profile($user_id, $meta_fields); // Pass sanitized meta fields + $organizer_id = $this->create_organizer_profile($user_id, $meta_fields, $org_logo_data); // Pass sanitized meta fields if ($organizer_id) { update_user_meta($user_id, 'hvac_organizer_id', $organizer_id); @@ -878,9 +1014,10 @@ class HVAC_Registration { * * @param int $user_id The user ID. * @param array $meta_data Array of sanitized user meta data. + * @param array|null $org_logo_data The $_FILES entry for the organization logo, if provided. * @return int|false Organizer Post ID on success, false on failure. */ - private function create_organizer_profile($user_id, $meta_data) { + private function create_organizer_profile($user_id, $meta_data, $org_logo_data = null) { if (!class_exists('Tribe__Events__Main') || !function_exists('tribe_create_organizer')) { return false; @@ -917,6 +1054,33 @@ class HVAC_Registration { return false; } + + // Store custom fields as post meta + if ($organizer_id) { + // Store headquarters location + if (!empty($meta_data['org_headquarters_city'])) { + update_post_meta($organizer_id, '_hvac_org_headquarters_city', $meta_data['org_headquarters_city']); + } + if (!empty($meta_data['org_headquarters_state'])) { + update_post_meta($organizer_id, '_hvac_org_headquarters_state', $meta_data['org_headquarters_state']); + } + if (!empty($meta_data['org_headquarters_country'])) { + update_post_meta($organizer_id, '_hvac_org_headquarters_country', $meta_data['org_headquarters_country']); + } + + // Store training capabilities + update_post_meta($organizer_id, '_hvac_business_type', $meta_data['business_type']); + update_post_meta($organizer_id, '_hvac_training_audience', $meta_data['training_audience']); + update_post_meta($organizer_id, '_hvac_training_formats', $meta_data['training_formats']); + update_post_meta($organizer_id, '_hvac_training_locations', $meta_data['training_locations']); + update_post_meta($organizer_id, '_hvac_training_resources', $meta_data['training_resources']); + update_post_meta($organizer_id, '_hvac_annual_revenue_target', $meta_data['annual_revenue_target']); + + // Handle logo upload + if ($org_logo_data) { + $this->handle_org_logo_upload($organizer_id, $org_logo_data); + } + } return (int) $organizer_id; } @@ -936,18 +1100,21 @@ class HVAC_Registration { // Use the already processed state/province from meta $state_province = $meta_data['user_state']; + + // Determine venue name + $venue_name = !empty($meta_data['venue_name']) ? $meta_data['venue_name'] : $meta_data['business_name'] . ' of ' . $meta_data['user_city']; $venue_data = array( - 'Venue' => $meta_data['business_name'] . ' Training Venue', // Venue name from sanitized meta + 'Venue' => $venue_name, 'Country' => $meta_data['user_country'], - 'Address' => '', // TEC doesn't have a single address line, use City/State/Zip + 'Address' => $meta_data['venue_address'], // Now we have a specific address field 'City' => $meta_data['user_city'], 'StateProvince' => $state_province, 'State' => $state_province, // Also set State field 'Province' => $state_province, // Also set Province field 'Zip' => $meta_data['user_zip'], - 'Phone' => $meta_data['business_phone'], - 'Website' => $meta_data['business_website'], + 'Phone' => !empty($meta_data['venue_phone']) ? $meta_data['venue_phone'] : $meta_data['business_phone'], + 'Website' => !empty($meta_data['venue_website']) ? $meta_data['venue_website'] : $meta_data['business_website'], 'post_status' => 'publish', // Publish venue immediately 'post_author' => $user_id // Associate with the new user ); @@ -1007,7 +1174,7 @@ class HVAC_Registration { $first_name = $user_info->first_name ?: sanitize_text_field($data['first_name']); $last_name = $user_info->last_name ?: sanitize_text_field($data['last_name']); $message .= "Name: " . $first_name . " " . $last_name . "\n"; - $message .= "Business Name: " . sanitize_text_field($data['business_name']) . "\n"; // Use raw data as meta might not be fully updated yet? Safer to use raw. + $message .= "Organization Name: " . sanitize_text_field($data['business_name']) . "\n"; // Use raw data as meta might not be fully updated yet? Safer to use raw. $message .= "Application Details:\n" . wp_kses_post($data['application_details']) . "\n\n"; // Use wp_kses_post for safety // Add link to user profile in admin diff --git a/includes/class-hvac-trainer-profile-manager.php b/includes/class-hvac-trainer-profile-manager.php new file mode 100644 index 00000000..89e4492d --- /dev/null +++ b/includes/class-hvac-trainer-profile-manager.php @@ -0,0 +1,537 @@ + admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('hvac_profile_nonce') + )); + + // Enqueue media uploader for profile photo + if (is_page('trainer/profile/edit')) { + wp_enqueue_media(); + } + } + } + + /** + * Render profile view + */ + public function render_profile_view() { + if (!is_user_logged_in() || !current_user_can('hvac_trainer')) { + return '

    You must be logged in as a trainer to view this page.

    '; + } + + $user_id = get_current_user_id(); + $user = get_userdata($user_id); + + // Get user meta + $phone = get_user_meta($user_id, 'user_phone', true); + $city = get_user_meta($user_id, 'user_city', true); + $state = get_user_meta($user_id, 'user_state', true); + $country = get_user_meta($user_id, 'user_country', true); + $linkedin = get_user_meta($user_id, 'user_linkedin', true); + $certifications = get_user_meta($user_id, 'trainer_certifications', true); + $years_experience = get_user_meta($user_id, 'years_experience', true); + $profile_photo_id = get_user_meta($user_id, 'profile_photo_id', true); + + // Get organization info + $organizer_id = get_user_meta($user_id, 'organizer_id', true); + $organization = null; + if ($organizer_id) { + $organization = get_post($organizer_id); + } + + ob_start(); + ?> +
    +
    +

    Trainer Profile

    + Edit Profile +
    + +
    + Trainer > Profile > View +
    + +
    +
    +
    + + $user->display_name)); ?> + +
    + first_name, 0, 1) . substr($user->last_name, 0, 1)); ?> +
    + +
    + +
    +
    + get_trainer_event_count($user_id); ?> + Events Created +
    +
    + get_trainer_student_count($user_id); ?> + Students Trained +
    + +
    + + Years Experience +
    + +
    +
    + +
    +
    +

    Personal Information

    +
    +
    + Name: + first_name . ' ' . $user->last_name); ?> +
    +
    + Email: + user_email); ?> +
    + +
    + Phone: + +
    + +
    + Location: + + + +
    + +
    + LinkedIn: + + View Profile + +
    + +
    +
    + + description)): ?> +
    +

    About

    +
    + description)); ?> +
    +
    + + + +
    +

    Training Organization

    +
    +
    + Organization: + post_title); ?> +
    + +
    + Headquarters: + + + +
    + +
    +
    + + + +
    +

    Certifications

    +
    + +
    + + +
    + +
    +
    + +
    +
    +
    + You must be logged in as a trainer to view this page.

    '; + } + + $user_id = get_current_user_id(); + $user = get_userdata($user_id); + + // Get user meta + $phone = get_user_meta($user_id, 'user_phone', true); + $city = get_user_meta($user_id, 'user_city', true); + $state = get_user_meta($user_id, 'user_state', true); + $country = get_user_meta($user_id, 'user_country', true); + $linkedin = get_user_meta($user_id, 'user_linkedin', true); + $website = $user->user_url; + $certifications = get_user_meta($user_id, 'trainer_certifications', true); + $years_experience = get_user_meta($user_id, 'years_experience', true); + $profile_photo_id = get_user_meta($user_id, 'profile_photo_id', true); + + ob_start(); + ?> +
    +
    +

    Edit Profile

    +
    + +
    + Trainer > Profile > Edit +
    + +
    + + +
    +

    Profile Photo

    +
    +
    + + + +
    No photo uploaded
    + +
    +
    + + + + + +
    +
    +
    + +
    +

    Personal Information

    + +
    +
    + + +
    +
    + + +
    +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + +
    +

    Location

    + +
    + + +
    + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +

    Professional Information

    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    + +
    + + Cancel +
    +
    +
    + $user_id, + 'first_name' => sanitize_text_field($_POST['first_name']), + 'last_name' => sanitize_text_field($_POST['last_name']), + 'display_name' => sanitize_text_field($_POST['display_name']), + 'user_email' => sanitize_email($_POST['email']), + 'user_url' => esc_url_raw($_POST['website']), + 'description' => wp_kses_post($_POST['description']) + ); + + $result = wp_update_user($user_data); + + if (is_wp_error($result)) { + wp_send_json_error($result->get_error_message()); + } + + // Update user meta + update_user_meta($user_id, 'user_phone', sanitize_text_field($_POST['phone'])); + update_user_meta($user_id, 'user_city', sanitize_text_field($_POST['city'])); + update_user_meta($user_id, 'user_state', sanitize_text_field($_POST['state'])); + update_user_meta($user_id, 'user_country', sanitize_text_field($_POST['country'])); + update_user_meta($user_id, 'user_linkedin', esc_url_raw($_POST['linkedin'])); + update_user_meta($user_id, 'years_experience', intval($_POST['years_experience'])); + update_user_meta($user_id, 'trainer_certifications', sanitize_textarea_field($_POST['certifications'])); + + // Update profile photo if changed + if (isset($_POST['profile_photo_id'])) { + update_user_meta($user_id, 'profile_photo_id', intval($_POST['profile_photo_id'])); + } + + wp_send_json_success('Profile updated successfully.'); + } + + /** + * AJAX handler for uploading profile photo + */ + public function ajax_upload_profile_photo() { + check_ajax_referer('hvac_profile_nonce', 'nonce'); + + if (!current_user_can('hvac_trainer')) { + wp_send_json_error('Unauthorized'); + } + + if (!isset($_FILES['profile_photo'])) { + wp_send_json_error('No file uploaded'); + } + + require_once(ABSPATH . 'wp-admin/includes/image.php'); + require_once(ABSPATH . 'wp-admin/includes/file.php'); + require_once(ABSPATH . 'wp-admin/includes/media.php'); + + $attachment_id = media_handle_upload('profile_photo', 0); + + if (is_wp_error($attachment_id)) { + wp_send_json_error($attachment_id->get_error_message()); + } + + // Update user meta + update_user_meta(get_current_user_id(), 'profile_photo_id', $attachment_id); + + wp_send_json_success(array( + 'attachment_id' => $attachment_id, + 'url' => wp_get_attachment_image_url($attachment_id, 'thumbnail') + )); + } + + /** + * Get trainer event count + */ + private function get_trainer_event_count($user_id) { + $post_type = class_exists('Tribe__Events__Main') ? Tribe__Events__Main::POSTTYPE : 'tribe_events'; + + $count = count_user_posts($user_id, $post_type); + + return $count; + } + + /** + * Get trainer student count + */ + private function get_trainer_student_count($user_id) { + global $wpdb; + + // Get all events by this trainer + $post_type = class_exists('Tribe__Events__Main') ? Tribe__Events__Main::POSTTYPE : 'tribe_events'; + + $events = get_posts(array( + 'post_type' => $post_type, + 'author' => $user_id, + 'posts_per_page' => -1, + 'fields' => 'ids' + )); + + if (empty($events)) { + return 0; + } + + // Count attendees across all events + $attendee_count = 0; + foreach ($events as $event_id) { + $attendees = get_post_meta($event_id, '_tribe_tickets_attendees', true); + if (is_array($attendees)) { + $attendee_count += count($attendees); + } + } + + return $attendee_count; + } +} \ No newline at end of file diff --git a/includes/class-hvac-venues.php b/includes/class-hvac-venues.php new file mode 100644 index 00000000..32e35b1a --- /dev/null +++ b/includes/class-hvac-venues.php @@ -0,0 +1,536 @@ + admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('hvac_venues_nonce') + )); + } + } + + /** + * Render venues list + */ + public function render_venues_list() { + if (!is_user_logged_in() || !current_user_can('hvac_trainer')) { + return '

    You must be logged in as a trainer to view this page.

    '; + } + + ob_start(); + ?> +
    +
    +

    Training Venues

    + Add New Venue +
    + +
    + Trainer > Venues > List +
    + + render_venues_table(); ?> +
    + class_exists('Tribe__Events__Main') ? Tribe__Events__Main::VENUE_POST_TYPE : 'tribe_venue', + 'posts_per_page' => $per_page, + 'offset' => $offset, + 'orderby' => 'title', + 'order' => 'ASC', + 'post_status' => 'publish' + ); + + // Filter handling + if (!empty($_GET['search'])) { + $query_args['s'] = sanitize_text_field($_GET['search']); + } + + if (!empty($_GET['state'])) { + $query_args['meta_query'] = array( + array( + 'key' => '_VenueStateProvince', + 'value' => sanitize_text_field($_GET['state']), + 'compare' => '=' + ) + ); + } + + // Get venues + $venues_query = new WP_Query($query_args); + + // Get total count for pagination + $total_venues = $venues_query->found_posts; + $total_pages = ceil($total_venues / $per_page); + + ?> +
    +
    +
    +
    + +
    +
    + +
    +
    + + Clear +
    +
    +
    +
    + +
    + + + + + + + + + + + + + have_posts()) { + while ($venues_query->have_posts()) { + $venues_query->the_post(); + $venue_id = get_the_ID(); + $is_author = (get_post_field('post_author', $venue_id) == $current_user_id); + + // Get venue meta + $address = get_post_meta($venue_id, '_VenueAddress', true); + $city = get_post_meta($venue_id, '_VenueCity', true); + $state = get_post_meta($venue_id, '_VenueStateProvince', true); + $phone = get_post_meta($venue_id, '_VenuePhone', true); + ?> + + + + + + + + + + + + + + +
    Venue NameAddressCityStatePhoneActions
    + + + Your Venue + + + + Edit + + View Only + +
    No venues found.
    +
    + + 1): ?> +
    + add_query_arg('paged', '%#%'), + 'format' => '', + 'current' => $page, + 'total' => $total_pages, + 'prev_text' => '« Previous', + 'next_text' => 'Next »' + )); + ?> +
    + + You must be logged in as a trainer to view this page.

    '; + } + + $venue_id = isset($_GET['venue_id']) ? intval($_GET['venue_id']) : 0; + $venue = null; + + if ($venue_id) { + $venue = get_post($venue_id); + + // Check if user can edit this venue + if (!$venue || $venue->post_author != get_current_user_id()) { + return '

    You do not have permission to edit this venue.

    '; + } + } + + ob_start(); + ?> +
    +
    +

    +
    + +
    + Trainer > + Venues > + +
    + +
    + + + +
    +

    Venue Information

    + +
    + + +
    + +
    + + +
    +
    + +
    +

    Location Details

    + +
    + + +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +

    Contact Information

    + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    + + Cancel + + + + +
    +
    +
    + post_author != get_current_user_id()) { + wp_send_json_error('You do not have permission to edit this venue.'); + } + } + + // Prepare venue data + $venue_data = array( + 'Venue' => sanitize_text_field($_POST['venue_name']), + 'Description' => wp_kses_post($_POST['venue_description']), + 'Address' => sanitize_text_field($_POST['venue_address']), + 'City' => sanitize_text_field($_POST['venue_city']), + 'StateProvince' => sanitize_text_field($_POST['venue_state']), + 'State' => sanitize_text_field($_POST['venue_state']), + 'Province' => sanitize_text_field($_POST['venue_state']), + 'Zip' => sanitize_text_field($_POST['venue_zip']), + 'Country' => sanitize_text_field($_POST['venue_country']), + 'Phone' => sanitize_text_field($_POST['venue_phone']), + 'URL' => esc_url_raw($_POST['venue_website']), + 'ShowMap' => true, + 'ShowMapLink' => true + ); + + if ($venue_id) { + $venue_data['ID'] = $venue_id; + $result = function_exists('tribe_update_venue') ? + tribe_update_venue($venue_id, $venue_data) : + wp_update_post($venue_data); + } else { + $venue_data['post_status'] = 'publish'; + $venue_data['post_author'] = get_current_user_id(); + $result = function_exists('tribe_create_venue') ? + tribe_create_venue($venue_data) : + wp_insert_post($venue_data); + } + + if (is_wp_error($result)) { + wp_send_json_error($result->get_error_message()); + } else { + wp_send_json_success(array( + 'message' => $venue_id ? 'Venue updated successfully.' : 'Venue created successfully.', + 'venue_id' => $result + )); + } + } + + /** + * AJAX handler for deleting venue + */ + public function ajax_delete_venue() { + check_ajax_referer('hvac_venues_nonce', 'nonce'); + + if (!current_user_can('hvac_trainer')) { + wp_send_json_error('Unauthorized'); + } + + $venue_id = isset($_POST['venue_id']) ? intval($_POST['venue_id']) : 0; + + if (!$venue_id) { + wp_send_json_error('Invalid venue ID'); + } + + $venue = get_post($venue_id); + if (!$venue || $venue->post_author != get_current_user_id()) { + wp_send_json_error('You do not have permission to delete this venue.'); + } + + // Check if venue is being used by any events + $events_using_venue = get_posts(array( + 'post_type' => class_exists('Tribe__Events__Main') ? Tribe__Events__Main::POSTTYPE : 'tribe_events', + 'meta_query' => array( + array( + 'key' => '_EventVenueID', + 'value' => $venue_id, + 'compare' => '=' + ) + ), + 'posts_per_page' => 1 + )); + + if (!empty($events_using_venue)) { + wp_send_json_error('Cannot delete venue. It is being used by one or more events.'); + } + + $result = wp_trash_post($venue_id); + + if ($result) { + wp_send_json_success('Venue deleted successfully.'); + } else { + wp_send_json_error('Failed to delete venue.'); + } + } + + /** + * AJAX handler for loading venue data + */ + public function ajax_load_venue() { + check_ajax_referer('hvac_venues_nonce', 'nonce'); + + if (!current_user_can('hvac_trainer')) { + wp_send_json_error('Unauthorized'); + } + + $venue_id = isset($_GET['venue_id']) ? intval($_GET['venue_id']) : 0; + + if (!$venue_id) { + wp_send_json_error('Invalid venue ID'); + } + + $venue = get_post($venue_id); + if (!$venue) { + wp_send_json_error('Venue not found'); + } + + $venue_data = array( + 'id' => $venue_id, + 'name' => $venue->post_title, + 'description' => $venue->post_content, + 'address' => get_post_meta($venue_id, '_VenueAddress', true), + 'city' => get_post_meta($venue_id, '_VenueCity', true), + 'state' => get_post_meta($venue_id, '_VenueStateProvince', true), + 'zip' => get_post_meta($venue_id, '_VenueZip', true), + 'country' => get_post_meta($venue_id, '_VenueCountry', true), + 'phone' => get_post_meta($venue_id, '_VenuePhone', true), + 'website' => get_post_meta($venue_id, '_VenueURL', true) + ); + + wp_send_json_success($venue_data); + } +} \ No newline at end of file diff --git a/templates/page-trainer-organizer-manage.php b/templates/page-trainer-organizer-manage.php new file mode 100644 index 00000000..dfbbb7ea --- /dev/null +++ b/templates/page-trainer-organizer-manage.php @@ -0,0 +1,20 @@ + + +
    +
    + +
    +
    + + + +
    +
    + +
    +
    + + + +
    +
    + +
    +
    + + + +
    +
    + +
    +
    + + + +
    +
    + +
    +
    + + + +
    +
    + +
    +
    + +