<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>CenterEdX Program Access Administration</title> <style> :root{ --navy:#08264f;--blue:#2c55b0;--pink:#c51d7d;--orange:#ff9f18; --ink:#17243a;--muted:#64748b;--line:#dce5f0;--page:#f5f8fc; --white:#fff;--green:#147d55;--green-bg:#e9f8f1; --amber:#8a5700;--amber-bg:#fff5df;--red:#9b2d3a;--red-bg:#fff0f2; --info:#174a91;--info-bg:#eef5ff;--shadow:0 16px 40px rgba(8,38,79,.10); } *{box-sizing:border-box} body{margin:0;background:var(--page);color:var(--ink);font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;line-height:1.45} button,input,select,textarea{font:inherit}button{cursor:pointer} .shell{max-width:1440px;margin:0 auto;padding:20px} .connection{border:1px solid #bfd6fa;background:var(--info-bg);color:var(--info);border-radius:14px;padding:13px 16px;margin-bottom:16px;font-weight:800} .connection.error{background:var(--red-bg);border-color:#efb5bd;color:#812632} .hero{display:grid;grid-template-columns:minmax(250px,440px) minmax(0,1fr);gap:25px;align-items:center;background:linear-gradient(118deg,var(--navy),#2956b5);color:#fff;border-radius:25px;padding:24px 28px;box-shadow:var(--shadow)} .logo-box{background:#fff;border-radius:19px;padding:12px 16px}.logo-box img{display:block;width:100%;height:100px;object-fit:contain} .hero h1{margin:0;font-size:clamp(31px,4vw,50px);line-height:1.02;letter-spacing:-.035em}.hero p{margin:11px 0 0;color:#dce9ff;font-size:17px} .hero-meta{display:flex;gap:10px;flex-wrap:wrap;margin-top:20px}.chip{display:inline-flex;padding:9px 13px;border-radius:999px;background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.22);font-weight:800} .main{margin-top:20px}.panel{background:#fff;border:1px solid var(--line);border-radius:22px;box-shadow:var(--shadow);padding:23px} .notice{border:1px solid;border-radius:15px;padding:14px 16px;margin:0 0 16px}.notice.info{background:var(--info-bg);border-color:#bfd6fa;color:#173f82}.notice.warn{background:var(--amber-bg);border-color:#eed08c;color:#6e4700}.notice.error{background:var(--red-bg);border-color:#efb5bd;color:#812632}.notice.success{background:var(--green-bg);border-color:#a7dbc6;color:#0d6545} .stats{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:11px;margin-bottom:18px}.stat{background:#fff;border:1px solid var(--line);border-radius:16px;padding:15px;box-shadow:0 7px 20px rgba(8,38,74,.06)}.stat b{display:block;font-size:28px;color:var(--navy)}.stat span{font-size:12px;color:var(--muted);font-weight:800} .panel-head{display:flex;justify-content:space-between;gap:18px;align-items:flex-start;margin-bottom:18px}h2,h3{margin:0;color:var(--navy)}h2{font-size:27px}h3{font-size:19px}.sub{margin:6px 0 0;color:var(--muted)} .tabs{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:18px}.tab{border:1px solid var(--line);background:#fff;color:var(--navy);padding:10px 14px;border-radius:999px;font-weight:850}.tab.active{background:var(--navy);color:#fff;border-color:var(--navy)}.tab-panel{display:none}.tab-panel.active{display:block} .app-list{display:grid;gap:13px}.app-card{border:1px solid var(--line);border-radius:17px;padding:17px;background:#fff}.app-card.focus{border:3px solid var(--pink);box-shadow:0 0 0 5px rgba(197,29,125,.12)}.app-top{display:flex;justify-content:space-between;gap:16px;align-items:flex-start}.meta{display:flex;flex-wrap:wrap;gap:7px;margin-top:8px;color:var(--muted);font-size:13px}.meta span{background:#f3f6fa;padding:5px 8px;border-radius:8px}.details{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:14px}.details div{font-size:13px}.details b{display:block;color:#334761;margin-bottom:3px} .badge{display:inline-flex;padding:6px 10px;border-radius:999px;font-size:12px;font-weight:850}.badge.pending{background:var(--amber-bg);color:var(--amber)}.badge.approved{background:var(--green-bg);color:var(--green)}.badge.declined,.badge.withdrawn{background:var(--red-bg);color:var(--red)} .actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:17px}.btn{border:0;border-radius:12px;padding:10px 15px;font-weight:850} .badge.reviewed{background:var(--green-bg);color:var(--green)} .btn{cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;justify-content:center} .coach-toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;margin-bottom:16px;padding:14px;background:#f5f8fc;border:1px solid var(--line);border-radius:14px} .coach-toolbar p{margin:0;color:var(--muted);font-size:14px} .answer-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:14px} .answer{padding:12px;background:#f7f9fc;border:1px solid #e2e9f2;border-radius:12px;font-size:13px;min-width:0} .answer.full{grid-column:1/-1} .answer b{display:block;color:#334761;margin-bottom:5px} .answer span{display:block;white-space:pre-wrap;overflow-wrap:anywhere} .coach-notes{margin-top:14px} .coach-notes label{display:block;margin-bottom:6px} .btn.primary{background:var(--blue);color:#fff}.btn.secondary{background:#e8eef8;color:var(--navy)}.btn.success{background:var(--green);color:#fff}.btn.danger{background:var(--red);color:#fff}.btn.small{padding:7px 10px;border-radius:9px;font-size:12px} .grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:15px}.field{display:flex;flex-direction:column;gap:6px}.field.full{grid-column:1/-1}label{font-size:14px;font-weight:800;color:#293a56}input,select,textarea{width:100%;border:1px solid #cbd8e7;background:#fff;border-radius:12px;padding:11px 12px;color:var(--ink);outline:none}input:focus,select:focus,textarea:focus{border-color:#3d68c2;box-shadow:0 0 0 3px rgba(61,104,194,.13)}textarea{min-height:94px;resize:vertical} .table-wrap{overflow:auto;border:1px solid var(--line);border-radius:15px}table{width:100%;border-collapse:collapse;min-width:900px}th,td{text-align:left;padding:12px;border-bottom:1px solid var(--line);vertical-align:top}th{background:#f2f6fb;color:var(--navy);font-size:12px;text-transform:uppercase;letter-spacing:.04em}tr:last-child td{border-bottom:0}.empty{padding:31px;text-align:center;color:var(--muted);border:1px dashed #c8d5e4;border-radius:15px;background:#fbfcfe} .busy{position:fixed;inset:0;background:rgba(245,248,252,.72);display:none;place-items:center;z-index:50;backdrop-filter:blur(2px)}.busy.show{display:grid}.spinner{width:45px;height:45px;border:4px solid #cbd8e7;border-top-color:var(--pink);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.toast{position:fixed;right:22px;bottom:22px;max-width:420px;background:var(--navy);color:#fff;border-radius:13px;padding:14px 16px;box-shadow:0 16px 40px rgba(0,0,0,.23);transform:translateY(130px);opacity:0;transition:.25s;z-index:60}.toast.show{transform:translateY(0);opacity:1}.toast.error{background:var(--red)} @media(max-width:1200px){.stats{grid-template-columns:repeat(4,1fr)}.details{grid-template-columns:repeat(2,1fr)}} @media(max-width:760px){.shell{padding:10px}.hero{grid-template-columns:1fr;padding:20px;border-radius:19px}.logo-box{max-width:430px}.stats,.grid,.details,.answer-grid{grid-template-columns:1fr}.answer.full{grid-column:auto}.field.full{grid-column:auto}.panel{padding:17px;border-radius:18px}.panel-head,.app-top{display:grid}} </style> </head> <body> <div class="busy" id="busy"><div class="spinner"></div></div> <div class="toast" id="toast"></div> <div class="shell"> <div class="connection" id="connectionBanner">Connecting to the live CenterEdX admin system…</div> <header class="hero"> <div class="logo-box"><img src="https://static.wixstatic.com/media/6a5cc9_07225f457a5744b99dd700dacbf3808a~mv2.png" alt="CGSE Center for Educational Excellence"></div> <div> <h1>CenterEdX Member Access</h1> <p>Manage member roles and pairings, and review or export CASC coach applications in a separate workspace.</p> <div class="hero-meta"> <span class="chip" id="memberChip">Connecting to Wix…</span> <span class="chip">Audited system v13 · CASC + TIP pairing</span> </div> </div> </header> <main class="main" id="app"></main> </div> <script> const app = document.getElementById('app'); const connectionBanner = document.getElementById('connectionBanner'); const memberChip = document.getElementById('memberChip'); const busy = document.getElementById('busy'); const toastElement = document.getElementById('toast'); const state = { bootstrap: null, tab: 'pending', focus: { applicationId: '', action: '', view: '' }, connected: false, handshakeInterval: null, handshakeTimeout: null }; const esc = (value) => String(value ?? '').replace(/[&<>'"]/g, (character) => ({'&':'&','<':'<','>':'>',"'":''','"':'"'})[character] ); const fmtDate = (value) => { if (!value) return '—'; const date = new Date(value); return Number.isNaN(date.getTime()) ? '—' : date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); }; function setBusy(on) { busy.classList.toggle('show', Boolean(on)); } function toast(message, isError = false) { toastElement.textContent = message; toastElement.className = `toast show${isError ? ' error' : ''}`; clearTimeout(toast.timer); toast.timer = setTimeout( () => toastElement.classList.remove('show'), 4500 ); } function post(type, payload = {}) { setBusy(true); window.parent.postMessage( { type, payload, requestId: Date.now().toString(36) + Math.random().toString(36).slice(2) }, '*' ); } function sendReady() { if (state.connected) return; window.parent.postMessage( { type: 'READY', requestId: 'v13-' + Date.now() }, '*' ); } function statusBadge(status) { const safe = String(status || 'pending').toLowerCase(); const label = safe.charAt(0).toUpperCase() + safe.slice(1); return `<span class="badge ${esc(safe)}">${esc(label)}</span>`; } function roleLabel(role) { return { casc_candidate: 'CASC Candidate', casc_coach: 'CASC Coach', tip_candidate: 'TIP Candidate', tip_coach: 'TIP Coach' }[role] || 'Program Member'; } function stat(value, label) { return `<div class="stat"><b>${Number(value || 0)}</b><span>${esc(label)}</span></div>`; } function renderApplicationCard(item, pending = false) { const role = item.requestedRole || item.activeRole; return ` <article class="app-card ${state.focus.applicationId === item.id ? 'focus' : ''}"> <div class="app-top"> <div> <h3>${esc(item.applicantName)}</h3> <div class="meta"> <span>${esc(roleLabel(role))}</span> <span>${esc(item.applicantEmail || 'No email')}</span> <span>${esc(item.districtOrganization || 'No district')}</span> <span>Submitted ${fmtDate(item.submittedAt)}</span> </div> </div> ${statusBadge(item.status)} </div> <div class="details"> <div><b>Program</b>${esc(item.program || '—')}</div> <div><b>School/site</b>${esc(item.schoolSite || '—')}</div> <div><b>Position</b>${esc(item.positionTitle || '—')}</div> <div><b>Cohort</b>${esc(item.requestedCohort || '—')}</div> </div> ${item.reviewNotes ? `<div class="notice info"><strong>Review notes:</strong> ${esc(item.reviewNotes)}</div>` : ''} ${pending ? ` <div class="actions"> <button class="btn success small" data-approve="${esc(role)}" data-id="${esc(item.id)}">Approve ${esc(roleLabel(role))}</button> <button class="btn danger small" data-decline data-id="${esc(item.id)}">Decline</button> </div> ` : ''} </article> `; } function renderCoachApplicationCard(item) { const isReviewed = item.status === 'reviewed'; const emailHref = item.applicantEmail ? 'mailto:' + encodeURIComponent(item.applicantEmail) : ''; return ` <article class="app-card ${state.focus.applicationId === item.id ? 'focus' : ''}" data-coach-card="${esc(item.id)}"> <div class="app-top"> <div> <h3>${esc(item.applicantName)}</h3> <div class="meta"> <span>${esc(item.applicantEmail || 'No email')}</span> <span>${esc(item.districtOrganization || 'No district/organization')}</span> <span>Submitted ${fmtDate(item.submittedAt)}</span> </div> </div> ${statusBadge(item.status)} </div> <div class="answer-grid"> <div class="answer"><b>Position/title</b><span>${esc(item.positionTitle || '—')}</span></div> <div class="answer"><b>Grade levels served</b><span>${esc(item.gradeLevelsServed || '—')}</span></div> <div class="answer full"><b>Areas of expertise</b><span>${esc(item.areasOfExpertise || '—')}</span></div> <div class="answer full"><b>Coaching strengths</b><span>${esc(item.coachingStrengths || '—')}</span></div> <div class="answer full"><b>Applicant notes</b><span>${esc(item.applicantNotes || '—')}</span></div> <div class="answer"><b>Consent</b><span>${item.consentConfirmed ? 'Confirmed' : 'Not recorded'}</span></div> <div class="answer"><b>Confirmation email</b><span>${esc(item.confirmationEmailStatus || 'Not recorded')}</span></div> ${isReviewed ? ` <div class="answer"><b>Reviewed</b><span>${fmtDate(item.reviewedAt)}</span></div> <div class="answer"><b>Reviewed by</b><span>${esc(item.reviewedByName || '—')}</span></div> ` : ''} </div> <div class="coach-notes"> <label for="coach-notes-${esc(item.id)}">Internal review notes</label> <textarea id="coach-notes-${esc(item.id)}" data-coach-notes placeholder="Notes visible only to program administrators">${esc(item.reviewNotes || '')}</textarea> </div> <div class="actions"> ${emailHref ? `<a class="btn secondary small" href="${esc(emailHref)}">Email applicant</a>` : ''} <button class="btn ${isReviewed ? 'secondary' : 'success'} small" data-coach-action="${isReviewed ? 'reopen' : 'mark_reviewed'}" data-id="${esc(item.id)}">${isReviewed ? 'Return to new' : 'Mark reviewed'}</button> </div> </article> `; } function csvValue(value) { return '"' + String(value ?? '').replace(/"/g, '""') + '"'; } function exportCoachApplications() { const items = state.bootstrap?.coachApplications || []; if (!items.length) { toast('There are no coach applications to export.', true); return; } const headers = [ 'Submitted', 'Status', 'Applicant name', 'Applicant email', 'District/organization', 'Position/title', 'Grade levels served', 'Areas of expertise', 'Coaching strengths', 'Applicant notes', 'Consent confirmed', 'Reviewed', 'Reviewed by', 'Internal review notes', 'Confirmation email status' ]; const rows = items.map(item => [ item.submittedAt, item.status, item.applicantName, item.applicantEmail, item.districtOrganization, item.positionTitle, item.gradeLevelsServed, item.areasOfExpertise, item.coachingStrengths, item.applicantNotes, item.consentConfirmed ? 'Yes' : 'No', item.reviewedAt, item.reviewedByName, item.reviewNotes, item.confirmationEmailStatus ]); const csv = [headers, ...rows] .map(row => row.map(csvValue).join(',')) .join('\r\n'); const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const csvWindow = window.open(url, '_blank'); if (!csvWindow) { URL.revokeObjectURL(url); toast('The browser blocked the CSV export window. Allow pop-ups and try again.', true); return; } try { csvWindow.opener = null; } catch (_) { // The CSV window is already isolated by the browser. } setTimeout(() => URL.revokeObjectURL(url), 30000); toast('Coach applications CSV opened in a new tab.'); } function pairingData(program) { return state.bootstrap?.pairing?.[program] || { candidates: [], coaches: [], assignments: [], activeAssignments: [] }; } function renderPairing(program) { const data = pairingData(program); const candidates = data.candidates || []; const coaches = data.coaches || []; if (!candidates.length) { return `<div class="empty">There are no approved, unpaired ${program} candidates.</div>`; } if (!coaches.length) { return ` <div class="notice warn"> <strong>${candidates.length} approved ${program} ${candidates.length === 1 ? 'candidate is' : 'candidates are'} waiting for a coach.</strong> Approve at least one ${program} Coach before creating a pairing. </div> <div class="table-wrap"> <table> <thead><tr><th>Waiting candidate</th><th>Cohort</th><th>District</th><th>School/site</th></tr></thead> <tbody> ${candidates.map(candidate => ` <tr> <td><strong>${esc(candidate.name)}</strong><div class="sub">${esc(candidate.email || '')}</div></td> <td>${esc(candidate.cohort || '—')}</td> <td>${esc(candidate.districtOrganization || '—')}</td> <td>${esc(candidate.schoolSite || '—')}</td> </tr> `).join('')} </tbody> </table> </div> `; } return ` <form class="pair-form" data-program="${program}"> <input type="hidden" name="program" value="${program}"> <div class="grid"> <div class="field"> <label>${program} Candidate</label> <select name="candidateApplicationId" required> <option value="">Select candidate</option> ${candidates.map(candidate => ` <option value="${esc(candidate.applicationId)}"> ${esc(candidate.name)} — ${esc(candidate.cohort || candidate.districtOrganization || '')} </option> `).join('')} </select> </div> <div class="field"> <label>${program} Coach</label> <select name="coachApplicationId" required> <option value="">Select coach</option> ${coaches.map(coach => ` <option value="${esc(coach.applicationId)}"> ${esc(coach.name)} — ${Number(coach.currentAssignments || 0)} active </option> `).join('')} </select> </div> <div class="field"> <label>Cohort</label> <input name="cohort" placeholder="Example: Cohort XXIII"> </div> <div class="field"> <label>Program year</label> <select name="programYear"> <option value="">Not specified</option> <option value="Year 1">Year 1</option> <option value="Year 2">Year 2</option> </select> </div> <div class="field"> <label>Pairing start date</label> <input name="startDate" type="date" value="${new Date().toISOString().slice(0,10)}"> </div> <div class="field full"> <label>Pairing notes</label> <textarea name="pairingNotes"></textarea> </div> </div> <div class="actions"> <button class="btn primary" type="submit">Create ${program} pairing</button> </div> </form> `; } function renderAssignments(program) { const items = pairingData(program).assignments || []; if (!items.length) { return `<div class="empty">No ${program} pairing history yet.</div>`; } return ` <div class="table-wrap"> <table> <thead> <tr> <th>Status</th><th>Candidate</th><th>Coach</th><th>Cohort</th> <th>Program year</th><th>Start</th><th>Ended</th><th></th> </tr> </thead> <tbody> ${items.map(item => ` <tr> <td>${item.active ? '<span class="badge approved">Active</span>' : '<span class="badge declined">Ended</span>'}</td> <td><strong>${esc(item.candidateName)}</strong><div class="sub">${esc(item.candidateEmail || '')}</div></td> <td><strong>${esc(item.coachName || '—')}</strong><div class="sub">${esc(item.coachEmail || '')}</div></td> <td>${esc(item.cohort || '—')}</td> <td>${esc(item.programYear || '—')}</td> <td>${fmtDate(item.startDate)}</td> <td>${fmtDate(item.endedAt)}</td> <td> ${item.active ? ` <button class="btn danger small" data-deactivate data-candidate="${esc(item.candidateApplicationId)}" data-assignment="${esc(item.id)}" data-program="${program}"> End pairing </button> ` : ''} </td> </tr> `).join('')} </tbody> </table> </div> `; } function render() { const data = state.bootstrap; if (!data) return; memberChip.textContent = `${data.identity.displayName} · Program Admin`; const pending = (data.applications || []).filter(item => item.status === 'pending'); const approved = (data.applications || []).filter(item => item.status === 'approved'); const coachApplications = data.coachApplications || []; const quality = data.dataQuality || {}; const qualityNotice = Number(quality.invalidRecordsIgnored || 0) || Number(quality.duplicateRecordsIgnored || 0) ? `<div class="notice info"><strong>Data cleanup applied:</strong> ${Number(quality.invalidRecordsIgnored || 0)} invalid/setup records hidden and ${Number(quality.duplicateRecordsIgnored || 0)} duplicate member requests consolidated.</div>` : ''; const roleNotice = data.settings.missingRoleKeys?.length ? `<div class="notice error"><strong>Wix role IDs are incomplete.</strong> Missing: ${esc(data.settings.missingRoleKeys.join(', '))}.</div>` : ''; const emailNotice = !data.settings.notificationConfigured ? `<div class="notice warn"><strong>Email configuration needs attention.</strong> Approval actions still work, but verify the SendGrid sender and CGSE Admin recipients.</div>` : ''; app.innerHTML = ` ${qualityNotice} ${roleNotice} ${emailNotice} <div class="stats"> ${stat(data.counts.pending, 'Pending')} ${stat(data.counts.coachApplications, 'Coach applications')} ${stat(data.counts.cascCandidates, 'CASC candidates')} ${stat(data.counts.cascCoaches, 'CASC coaches')} ${stat(data.counts.cascPairings, 'CASC pairings')} ${stat(data.counts.tipCandidates, 'TIP candidates')} ${stat(data.counts.tipCoaches, 'TIP coaches')} ${stat(data.counts.tipPairings, 'TIP pairings')} </div> <section class="panel"> <div class="panel-head"> <div> <h2>${state.tab === 'coachApplications' ? 'Coach applications' : 'Program access and pairing'}</h2> <p class="sub">${state.tab === 'coachApplications' ? 'Review and export public CoachApp submissions. These records do not enter member-role approvals.' : 'Approve program roles first. Only approved candidates and coaches appear in the matching CASC or TIP pairing tab.'}</p> </div> <button class="btn secondary" data-refresh>Refresh</button> </div> <div class="tabs"> ${[ ['pending', `Pending (${pending.length})`], ['approved', `Approved (${approved.length})`], ['coachApplications', `Coach Applications (${coachApplications.length})`], ['cascPairing', `CASC Pairing (${pairingData('CASC').candidates.length} waiting)`], ['cascAssignments', `CASC Assignments (${pairingData('CASC').activeAssignments.length} active)`], ['tipPairing', `TIP Pairing (${pairingData('TIP').candidates.length} waiting)`], ['tipAssignments', `TIP Assignments (${pairingData('TIP').activeAssignments.length} active)`] ].map(([key,label]) => `<button class="tab ${state.tab === key ? 'active' : ''}" data-tab="${key}">${esc(label)}</button>`).join('')} </div> <div class="tab-panel ${state.tab === 'pending' ? 'active' : ''}"> ${pending.length ? `<div class="app-list">${pending.map(item => renderApplicationCard(item, true)).join('')}</div>` : '<div class="empty">No pending program-role requests.</div>'} </div> <div class="tab-panel ${state.tab === 'approved' ? 'active' : ''}"> ${approved.length ? `<div class="app-list">${approved.map(item => renderApplicationCard(item, false)).join('')}</div>` : '<div class="empty">No approved program members yet.</div>'} </div> <div class="tab-panel ${state.tab === 'coachApplications' ? 'active' : ''}"> <div class="coach-toolbar"> <p><strong>Coach applications are separate from member-role approvals and pairings.</strong> Review each submission here or export the full application list.</p> <button class="btn primary" data-export-coach>Export CSV</button> </div> ${coachApplications.length ? `<div class="app-list">${coachApplications.map(renderCoachApplicationCard).join('')}</div>` : '<div class="empty">No CoachApp submissions yet.</div>'} </div> <div class="tab-panel ${state.tab === 'cascPairing' ? 'active' : ''}">${renderPairing('CASC')}</div> <div class="tab-panel ${state.tab === 'cascAssignments' ? 'active' : ''}">${renderAssignments('CASC')}</div> <div class="tab-panel ${state.tab === 'tipPairing' ? 'active' : ''}">${renderPairing('TIP')}</div> <div class="tab-panel ${state.tab === 'tipAssignments' ? 'active' : ''}">${renderAssignments('TIP')}</div> </section> `; bind(); } function formObject(form) { const result = {}; for (const [key,value] of new FormData(form).entries()) result[key] = value; return result; } function bind() { document.querySelector('[data-refresh]')?.addEventListener('click', () => post('REFRESH_BOOTSTRAP')); document.querySelectorAll('[data-tab]').forEach(button => { button.addEventListener('click', () => { state.tab = button.dataset.tab; render(); }); }); document.querySelector('[data-export-coach]')?.addEventListener('click', exportCoachApplications); document.querySelectorAll('[data-coach-action]').forEach(button => { button.addEventListener('click', () => { const card = button.closest('[data-coach-card]'); const notes = card?.querySelector('[data-coach-notes]')?.value || ''; post('ADMIN_REVIEW_COACH_APPLICATION', { applicationId: button.dataset.id, action: button.dataset.coachAction, reviewNotes: notes }); }); }); document.querySelectorAll('[data-approve]').forEach(button => { button.addEventListener('click', () => { const role = button.dataset.approve; post('ADMIN_REVIEW_APPLICATION', { applicationId: button.dataset.id, decision: 'approve', approvedRole: role, reviewNotes: '' }); }); }); document.querySelectorAll('[data-decline]').forEach(button => { button.addEventListener('click', () => { const reason = prompt('Reason or instructions for the applicant:'); if (reason === null) return; post('ADMIN_REVIEW_APPLICATION', { applicationId: button.dataset.id, decision: 'decline', reviewNotes: reason }); }); }); document.querySelectorAll('.pair-form').forEach(form => { form.addEventListener('submit', event => { event.preventDefault(); post('ADMIN_CREATE_PAIRING', formObject(event.currentTarget)); }); }); document.querySelectorAll('[data-deactivate]').forEach(button => { button.addEventListener('click', () => { const reason = prompt(`Reason for ending this ${button.dataset.program} pairing:`); if (reason === null) return; post('ADMIN_DEACTIVATE_PAIRING', { candidateApplicationId: button.dataset.candidate, assignmentId: button.dataset.assignment, program: button.dataset.program, reason }); }); }); } function clearHandshake() { if (state.handshakeInterval) clearInterval(state.handshakeInterval); if (state.handshakeTimeout) clearTimeout(state.handshakeTimeout); state.handshakeInterval = null; state.handshakeTimeout = null; } window.addEventListener('message', event => { const message = event.data || {}; if (message.type === 'PAGE_READY') { sendReady(); return; } if (message.type === 'CONNECTION_ACK') { state.connected = true; clearHandshake(); connectionBanner.className = 'connection'; connectionBanner.textContent = 'Connected to Wix. Loading live approvals and pairing records…'; return; } if (message.type === 'BOOTSTRAP') { state.connected = true; clearHandshake(); setBusy(false); connectionBanner.style.display = 'none'; state.bootstrap = message.payload; state.focus = message.focus || state.focus; if (state.focus.view === 'coach-applications') state.tab = 'coachApplications'; render(); return; } if (message.type === 'ERROR') { setBusy(false); const text = message.payload?.message || message.message || 'The request failed.'; connectionBanner.style.display = 'block'; connectionBanner.className = 'connection error'; connectionBanner.textContent = text; toast(text, true); } }); sendReady(); state.handshakeInterval = setInterval(sendReady, 750); state.handshakeTimeout = setTimeout(() => { if (!state.connected) { clearHandshake(); connectionBanner.className = 'connection error'; connectionBanner.textContent = 'The approvals widget did not connect to the /approvals page code. Confirm the HTML element ID is htmlMemberAdmin and publish the page code included in the v13 package.'; } }, 30000); </script> </body> </html>