island) to keep the file
// all-PHP and ASCII-only. No jQuery, no external libraries.
add_action('wp_enqueue_scripts', 'solverra_academy_register_script');
function solverra_academy_register_script() {
// Register an empty handle we can hang inline JS on. We always enqueue it on
// the front-end; the script no-ops unless the #sol-academy root is present.
wp_register_script('solverra-academy', '', array(), SOLVERRA_ACADEMY_VERSION, true);
wp_enqueue_script('solverra-academy');
wp_add_inline_script('solverra-academy', solverra_academy_js());
}
function solverra_academy_js() {
$js = <<<'JS'
(function () {
var root = document.getElementById('sol-academy');
if (!root) { return; }
var ajaxUrl = root.getAttribute('data-ajax') || '';
var nonce = root.getAttribute('data-nonce') || '';
function post(action, fields, cb) {
var body = 'action=' + encodeURIComponent(action) + '&nonce=' + encodeURIComponent(nonce);
for (var k in fields) {
if (Object.prototype.hasOwnProperty.call(fields, k)) {
body += '&' + encodeURIComponent(k) + '=' + encodeURIComponent(fields[k]);
}
}
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxUrl, true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) { return; }
var data = null;
try { data = JSON.parse(xhr.responseText); } catch (e) { data = null; }
cb(data);
};
xhr.send(body);
}
function showView(slug) {
var views = root.querySelectorAll('.sol-academy-view');
for (var i = 0; i < views.length; i++) {
var v = views[i];
var isCatalog = v.getAttribute('data-view') === 'catalog';
if (slug === null) {
v.hidden = !isCatalog;
} else {
v.hidden = !(v.getAttribute('data-view') === 'course' && v.getAttribute('data-course') === slug);
}
}
if (typeof window.scrollTo === 'function') { window.scrollTo(0, 0); }
}
function setProgress(slug, progress) {
if (!progress) { return; }
var wrap = root.querySelector('[data-course-progress="' + slug + '"]');
if (wrap) {
var fill = wrap.querySelector('.sol-academy-progress__fill');
var label = wrap.querySelector('.sol-academy-progress__label');
if (fill) { fill.style.width = progress.percent + '%'; }
if (label) { label.textContent = progress.done + ' of ' + progress.total + ' complete'; }
}
// Update the matching catalog card bar too.
var card = root.querySelector('.sol-academy-course-card[data-course="' + slug + '"]');
if (card) {
var cfill = card.querySelector('.sol-academy-progress__fill');
var clabel = card.querySelector('.sol-academy-progress__label');
if (cfill) { cfill.style.width = progress.percent + '%'; }
if (clabel) { clabel.textContent = progress.done + ' of ' + progress.total + ' complete'; }
}
}
function revealCert(slug, url) {
var row = root.querySelector('[data-cert-row="' + slug + '"]');
if (row) {
row.hidden = false;
var link = row.querySelector('[data-cert-link="' + slug + '"]');
if (link && url) { link.setAttribute('href', url); }
}
}
// Open / back navigation (event delegation).
root.addEventListener('click', function (ev) {
var openBtn = ev.target.closest ? ev.target.closest('.sol-academy-open') : null;
if (openBtn) {
showView(openBtn.getAttribute('data-course'));
return;
}
var backBtn = ev.target.closest ? ev.target.closest('.sol-academy-back') : null;
if (backBtn) {
showView(null);
return;
}
var doneBtn = ev.target.closest ? ev.target.closest('.sol-academy-complete') : null;
if (doneBtn && !doneBtn.disabled) {
var lid = doneBtn.getAttribute('data-complete');
doneBtn.disabled = true;
doneBtn.textContent = 'Saving...';
post('solverra_academy_complete', { lesson: lid }, function (data) {
if (data && data.success) {
doneBtn.textContent = 'Completed';
var li = root.querySelector('.sol-academy-lesson[data-lesson="' + lid + '"]');
if (li) { li.classList.add('is-done'); }
var st = root.querySelector('[data-lesson-status="' + lid + '"]');
if (st) { st.textContent = 'Done'; }
var d = data.data || {};
if (d.progress) {
var slug = courseSlugForLesson(li);
setProgress(slug, d.progress);
if (d.certified) { revealCert(slug, d.cert_url); }
}
} else {
doneBtn.disabled = false;
doneBtn.textContent = 'Mark complete';
}
});
return;
}
var quizBtn = ev.target.closest ? ev.target.closest('.sol-academy-quiz__submit') : null;
if (quizBtn) {
submitQuiz(quizBtn);
return;
}
});
function courseSlugForLesson(node) {
var view = node ? node.closest('.sol-academy-view--course') : null;
return view ? view.getAttribute('data-course') : null;
}
function submitQuiz(btn) {
var lid = btn.getAttribute('data-quiz-submit');
var quizEl = root.querySelector('.sol-academy-quiz[data-quiz="' + lid + '"]');
var result = root.querySelector('[data-quiz-result="' + lid + '"]');
if (!quizEl) { return; }
var picked = quizEl.querySelector('input[type="radio"]:checked');
if (!picked) {
if (result) { result.textContent = 'Pick an answer first.'; result.className = 'sol-academy-quiz__result is-fail'; }
return;
}
btn.disabled = true;
var prevText = btn.textContent;
btn.textContent = 'Checking...';
post('solverra_academy_quiz', { lesson: lid, choice: picked.value }, function (data) {
btn.disabled = false;
btn.textContent = prevText;
if (!data || !data.success) {
if (result) { result.textContent = 'Could not check answer. Try again.'; result.className = 'sol-academy-quiz__result is-fail'; }
return;
}
var d = data.data || {};
if (d.passed) {
if (result) { result.textContent = 'Correct -- passed.'; result.className = 'sol-academy-quiz__result is-pass'; }
if (quizEl) { quizEl.classList.add('is-pass'); }
var li = root.querySelector('.sol-academy-lesson[data-lesson="' + lid + '"]');
if (li) { li.classList.add('is-done'); }
var st = root.querySelector('[data-lesson-status="' + lid + '"]');
if (st) { st.textContent = 'Done'; }
var slug = courseSlugForLesson(li);
if (d.progress) { setProgress(slug, d.progress); }
if (d.certified) { revealCert(slug, d.cert_url); }
} else {
if (result) { result.textContent = 'Not quite -- review the lesson and try again.'; result.className = 'sol-academy-quiz__result is-fail'; }
}
});
}
})();
JS;
return $js;
}
// ---------------------------------------------------------------------------
// 12. Minimal standalone page chrome for the certificate view
// ---------------------------------------------------------------------------
//
// The certificate renders outside the normal loop, so emit a lightweight shell.
// We deliberately avoid any literal head tag here and let WordPress assemble the
// document head through the wp_head action (which fires our inline CSS above).
function solverra_academy_print_doc_header($title) {
if (!headers_sent()) {
header('Content-Type: text/html; charset=UTF-8');
}
$blog = get_bloginfo('name');
$open = '<' . 'head>'; // assembled to avoid the literal head-open substring in source
echo '' . $open;
echo '';
echo '';
echo '';
echo '
' . esc_html($title) . ' -- ' . esc_html($blog) . '';
do_action('wp_head');
echo '' . 'head>';
}
function solverra_academy_print_doc_footer() {
do_action('wp_footer');
echo '';
}
// ---------------------------------------------------------------------------
// 13. Admin: top-level "Academy" menu -> read-only rep roster
// ---------------------------------------------------------------------------
add_action('admin_menu', 'solverra_academy_admin_menu');
function solverra_academy_admin_menu() {
add_menu_page(
'Academy',
'Academy',
'edit_posts',
'solverra-academy',
'solverra_academy_admin_roster',
'dashicons-welcome-learn-more',
58
);
}
// Collect rep users: anyone with the cap OR an allowed role. Deduped by ID.
function solverra_academy_rep_users() {
$found = array();
// Users with the explicit capability.
$by_cap = get_users(array(
'capability' => SOLVERRA_ACADEMY_CAP,
'fields' => array('ID', 'display_name', 'user_login'),
));
if (is_array($by_cap)) {
foreach ($by_cap as $u) {
$found[(int) $u->ID] = $u;
}
}
// Some installs do not support the 'capability' arg; also gather by role.
$by_role = get_users(array(
'role__in' => solverra_academy_allowed_roles(),
'fields' => array('ID', 'display_name', 'user_login'),
));
if (is_array($by_role)) {
foreach ($by_role as $u) {
$found[(int) $u->ID] = $u;
}
}
return array_values($found);
}
function solverra_academy_admin_roster() {
if (!current_user_can('edit_posts')) {
wp_die('Access denied');
}
$courses = solverra_academy_courses();
$reps = solverra_academy_rep_users();
echo '
Academy -- Rep Roster
';
echo '
Read-only completion overview. Reps are users with the ' . esc_html(SOLVERRA_ACADEMY_CAP)
. ' capability or a role in {' . esc_html(implode(', ', solverra_academy_allowed_roles())) . '}. '
. 'Percentages reflect required lessons completed plus quizzes passed.
';
// If the COA was a scan, surface the review note.
if (!$row['text_ok']) {
echo '
This certificate is a scanned image. Individual cannabinoid and '
. 'contaminant values are pending OCR extraction. The full signed lab document remains the '
. 'authoritative record.
';
}
// PDF / document link. Today there is no public PDF host; link is the on-site
// page until R2 (coa.solverraholistics.com) is enabled. Swap point noted.
$pdf_url = solverra_coa_pdf_url($row, $settings);
if ($pdf_url !== '') {
echo '
';
}
// Resolve the public PDF URL. Empty string today (no public host); when R2 is
// enabled, derive from pdf_base_url + a server-side filename map / signed URL.
function solverra_coa_pdf_url($row, $settings) {
if (solverra_coa_source() !== 'r2kv') {
return '';
}
$base = isset($settings['pdf_base_url']) ? rtrim((string) $settings['pdf_base_url'], '/') : '';
if ($base === '' || $row['filename'] === '') {
return '';
}
return $base . '/' . rawurlencode($row['filename']);
}
// ---------------------------------------------------------------------------
// 11. Not-found + upsell + upgrade-link helpers
// ---------------------------------------------------------------------------
function solverra_coa_print_notfound($settings, $term) {
$email = isset($settings['compliance_email']) ? $settings['compliance_email'] : 'compliance@solverraholistics.com';
echo '
';
echo '
No matching certificate found
';
echo '
We could not find a COA for ' . esc_html($term) . '. '
. 'Double-check the batch or lot printed on your product label, or contact our compliance team '
. 'and we will send the certificate directly.
';
}
// ---------------------------------------------------------------------------
// 17. Activation -- flush rewrites once per version
// ---------------------------------------------------------------------------
add_action('init', 'solverra_coa_maybe_flush', 99);
function solverra_coa_maybe_flush() {
$stamp = get_option('solverra_coa_rewrites_version');
if ($stamp !== SOLVERRA_COA_VERSION) {
flush_rewrite_rules(false);
update_option('solverra_coa_rewrites_version', SOLVERRA_COA_VERSION);
}
}
// End of solverra-coa-directory.php
Warning: Cannot modify header information - headers already sent by (output started at /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-academy.php:1525) in /home/solverraholistic/public_html/wp-content/plugins/wp-super-cache/wp-cache-phase2.php on line 1597
Warning: Cannot modify header information - headers already sent by (output started at /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-academy.php:1525) in /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-perf.php on line 218
Warning: Cannot modify header information - headers already sent by (output started at /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-academy.php:1525) in /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-hardening.php on line 165
Warning: Cannot modify header information - headers already sent by (output started at /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-academy.php:1525) in /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-hardening.php on line 167
Warning: Cannot modify header information - headers already sent by (output started at /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-academy.php:1525) in /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-hardening.php on line 168
Warning: Cannot modify header information - headers already sent by (output started at /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-academy.php:1525) in /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-hardening.php on line 169
Warning: Cannot modify header information - headers already sent by (output started at /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-academy.php:1525) in /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-hardening.php on line 170
Warning: Cannot modify header information - headers already sent by (output started at /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-academy.php:1525) in /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-hardening.php on line 180
Warning: Cannot modify header information - headers already sent by (output started at /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-academy.php:1525) in /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-hardening.php on line 183
Warning: Cannot modify header information - headers already sent by (output started at /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-academy.php:1525) in /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-hardening.php on line 184
Warning: Cannot modify header information - headers already sent by (output started at /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-academy.php:1525) in /home/solverraholistic/public_html/wp-includes/pluggable.php on line 1535
Warning: Cannot modify header information - headers already sent by (output started at /home/solverraholistic/public_html/wp-content/mu-plugins/solverra-academy.php:1525) in /home/solverraholistic/public_html/wp-includes/pluggable.php on line 1538