const SUPABASE_URL = 'https://lywsyxgfbicfudutzsmv.supabase.co'; const SUPABASE_KEY = 'sb_publishable_Mf83FAWOu9UYeH3wBskg4A_CILLdZRc'; const api = axios.create({ baseURL: `${SUPABASE_URL}/rest/v1`, headers: { 'apikey': SUPABASE_KEY, 'Authorization': `Bearer ${SUPABASE_KEY}`, 'Content-Type': 'application/json' } }); const PASSPHRASE_HASH = '42356720f0561d28011ae4c4a674532d3434732a257d7afb1ce4f44458e29502'; let isUnlocked = false; let courses = []; let editingId = null; let sortCol = 'course_number'; let sortAsc = true; function showToast(message) { const container = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = 'toast'; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.classList.add('fade-out'); setTimeout(() => toast.remove(), 500); }, 3000); } async function loadData() { try { const res = await api.get('/courses?order=created_at.desc'); courses = res.data; document.getElementById('sync-stamp').textContent = "Last Sync: " + new Date().toLocaleTimeString(); populateDynamicFilters(); renderTable(); } catch (err) { showToast("Sync Error: " + err.message); } } function populateDynamicFilters() { const instF = document.getElementById('filter-instructor'); const statF = document.getElementById('filter-status'); const dsgnrF = document.getElementById('filter-designer'); const instructors = [...new Set(courses.map(c => c.instructor).filter(i => i))].sort(); const statuses = [...new Set(courses.map(c => c.development_status).filter(s => s))].sort(); const designers = [...new Set(courses.map(c => c.ins_dsgnr).filter(d => d))].sort(); instF.innerHTML = '' + instructors.map(i => ``).join(''); statF.innerHTML = '' + statuses.map(s => ``).join(''); dsgnrF.innerHTML = '' + designers.map(d => ``).join(''); } function updateStats(filteredData) { const totalCount = filteredData.length; const activeCount = filteredData.filter(c => c.active !== false).length; const sum = filteredData.reduce((acc, c) => acc + (Number(c.accessibility) || 0), 0); const avg = totalCount > 0 ? Math.round(sum / totalCount) : 0; const thresholdCount = filteredData.filter(c => Number(c.accessibility) >= 95).length; const templateCount = filteredData.filter(c => String(c.template || '').toLowerCase().trim() === 'yes').length; document.getElementById('stat-total').textContent = totalCount; document.getElementById('stat-active-count').textContent = `${activeCount} Active Courses`; document.getElementById('stat-access').textContent = avg + '%'; document.getElementById('stat-access-threshold').textContent = `${thresholdCount} of ${totalCount} at 95%+ threshold`; document.getElementById('stat-templates').textContent = templateCount; } function renderTable() { const q = document.getElementById('search-input').value.toLowerCase(); const prog = document.getElementById('filter-program').value; const inst = document.getElementById('filter-instructor').value; const dsgnr = document.getElementById('filter-designer').value; const stat = document.getElementById('filter-status').value; const act = document.getElementById('filter-active').value; const checkedTerms = Array.from(document.querySelectorAll('.term-filter:checked')).map(cb => cb.value); let filtered = courses.filter(c => { const matchQ = !q || [c.course_number, c.instructor, c.ins_dsgnr, c.course_abbrev, c.sum_shell_name, c.development_status].some(v => String(v || '').toLowerCase().includes(q)); const matchProg = !prog || c.online_program === prog; const matchInst = !inst || c.instructor === inst; const matchDsgnr = !dsgnr || c.ins_dsgnr === dsgnr; const matchStat = !stat || c.development_status === stat; const matchAct = act === '' || String(!!c.active) === act; const matchTerm = checkedTerms.includes(c.term || "TBD"); return matchQ && matchProg && matchInst && matchDsgnr && matchStat && matchAct && matchTerm; }); filtered.sort((a,b) => { let v1 = a[sortCol] ?? '', v2 = b[sortCol] ?? ''; return sortAsc ? String(v1).localeCompare(String(v2)) : String(v2).localeCompare(String(v1)); }); const tbody = document.getElementById('table-body'); tbody.innerHTML = ''; filtered.forEach(c => { const score = c.accessibility || 0; const tr = document.createElement('tr'); if (c.active === false) tr.className = 'heat-inactive'; const tdEdit = document.createElement('td'); if (isUnlocked) { const btn = document.createElement('button'); btn.style.cssText = "background:none;border:none;cursor:pointer;color:var(--navy)"; btn.innerHTML = ''; btn.onclick = () => editCourse(c.id); tdEdit.appendChild(btn); } else { tdEdit.textContent = '🔒'; } tr.appendChild(tdEdit); const tdPrefix = document.createElement('td'); tdPrefix.style.fontWeight = '700'; tdPrefix.textContent = c.course_abbrev || ''; tr.appendChild(tdPrefix); const tdNumber = document.createElement('td'); tdNumber.style.fontWeight = '700'; tdNumber.style.cursor = 'pointer'; tdNumber.style.color = 'var(--navy)'; tdNumber.style.textDecoration = 'underline'; tdNumber.textContent = c.course_number || ''; tdNumber.onclick = () => viewCourse(c.id); tr.appendChild(tdNumber); const tdShell = document.createElement('td'); tdShell.style.fontSize = '0.75rem'; tdShell.textContent = c.sum_shell_name || ''; tdShell.title = c.sum_shell_name || ''; const copyIcon = document.createElement('i'); copyIcon.className = "fa-regular fa-copy copy-btn"; copyIcon.onclick = () => copyToClipboard(c.sum_shell_name, copyIcon); tdShell.appendChild(copyIcon); tr.appendChild(tdShell); const tdInst = document.createElement('td'); tdInst.textContent = c.instructor || 'TBD'; tr.appendChild(tdInst); const tdProg = document.createElement('td'); const badge = document.createElement('span'); badge.className = 'badge'; badge.textContent = c.online_program || ''; tdProg.appendChild(badge); tr.appendChild(tdProg); const tdDsgnr = document.createElement('td'); tdDsgnr.textContent = c.ins_dsgnr || 'TBD'; tr.appendChild(tdDsgnr); const tdTerm = document.createElement('td'); tdTerm.textContent = c.term || 'TBD'; tr.appendChild(tdTerm); const tdAlly = document.createElement('td'); const allyWrap = document.createElement('div'); allyWrap.className = 'ally-wrap'; const bar = document.createElement('div'); bar.className = 'ally-bar'; if (score >= 95) bar.classList.add('bar-green'); else if (score >= 90) bar.classList.add('bar-yellow'); else bar.classList.add('bar-red'); const allyText = document.createElement('span'); allyText.style.fontWeight = '800'; allyText.textContent = `${score}%`; allyWrap.appendChild(bar); allyWrap.appendChild(allyText); tdAlly.appendChild(allyWrap); tr.appendChild(tdAlly); const tdLvl = document.createElement('td'); tdLvl.style.fontWeight = '600'; tdLvl.textContent = c.level || 0; tr.appendChild(tdLvl); const tdTemp = document.createElement('td'); if (String(c.template || '').toLowerCase() === 'yes') { tdTemp.innerHTML = ''; } else { tdTemp.innerHTML = ''; } tr.appendChild(tdTemp); const tdCopy = document.createElement('td'); if (String(c.Copy_upcoming_flag || '').toLowerCase() === 'yes') { tdCopy.innerHTML = ''; } else { tdCopy.innerHTML = ''; } tr.appendChild(tdCopy); const tdStat = document.createElement('td'); const tag = document.createElement('span'); tag.className = 'status-tag'; tag.textContent = c.development_status || 'Not Started'; tdStat.appendChild(tag); tr.appendChild(tdStat); const tdNotes = document.createElement('td'); tdNotes.style.cssText = "max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap"; tdNotes.title = c.notes || ''; tdNotes.textContent = c.notes || '--'; tr.appendChild(tdNotes); const tdDel = document.createElement('td'); if (isUnlocked) { const btn = document.createElement('button'); btn.style.cssText = "background:none;border:none;color:red;cursor:pointer"; btn.innerHTML = ''; btn.onclick = () => deleteCourse(c.id); tdDel.appendChild(btn); } tr.appendChild(tdDel); tbody.appendChild(tr); }); updateStats(filtered); updateSortUI(); } async function submitPin() { const input = document.getElementById('pin-input').value; const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input)); const hash = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join(''); if (hash === PASSPHRASE_HASH) { isUnlocked = true; closePinModal(); updateLockUI(); renderTable(); showToast("Authenticated Successfully"); } else { document.getElementById('pin-error').textContent = "Incorrect passphrase."; } } function updateLockUI() { document.getElementById('lock-banner').style.display = isUnlocked ? 'none' : 'flex'; document.getElementById('lock-label').textContent = isUnlocked ? 'Unlocked' : 'Locked'; document.getElementById('lock-icon').className = isUnlocked ? 'fa-solid fa-lock-open' : 'fa-solid fa-lock'; document.querySelectorAll('.write-control').forEach(el => el.classList.toggle('disabled', !isUnlocked)); } function handleLockClick() { if (isUnlocked) { isUnlocked = false; updateLockUI(); renderTable(); showToast("Locked for Viewing"); } else { document.getElementById('pin-overlay').classList.add('active'); } } function closePinModal() { document.getElementById('pin-overlay').classList.remove('active'); } function viewCourse(id) { const c = courses.find(x => x.id == id); if (!c) return; document.getElementById('v-course-name').textContent = `${c.course_abbrev || ''} ${c.course_number || ''}`; document.getElementById('v-prog').textContent = c.online_program || 'N/A'; document.getElementById('v-term').textContent = c.term || 'TBD'; document.getElementById('v-inst').textContent = c.instructor || 'TBD'; document.getElementById('v-dsgnr').textContent = c.ins_dsgnr || 'TBD'; document.getElementById('v-stat').textContent = c.development_status || 'Not Started'; document.getElementById('v-lvl').textContent = c.level || 0; document.getElementById('v-temp').textContent = c.template || 'No'; document.getElementById('v-copy').textContent = c.Copy_upcoming_flag || 'No'; document.getElementById('v-shell').textContent = c.sum_shell_name || 'N/A'; document.getElementById('v-ally').textContent = `${c.accessibility || 0}%`; document.getElementById('v-notes').textContent = c.notes || 'No notes provided.'; editingId = c.id; document.getElementById('v-edit-btn').style.display = isUnlocked ? 'inline-flex' : 'none'; document.getElementById('view-overlay').classList.add('active'); } function closeViewModal() { document.getElementById('view-overlay').classList.remove('active'); } function goToEdit() { closeViewModal(); editCourse(editingId); } function openModal(course = null) { if (!isUnlocked && !course) return; editingId = course?.id || null; document.getElementById('f-abbrev').value = course?.course_abbrev || ''; document.getElementById('f-number').value = course?.course_number || ''; document.getElementById('f-instructor').value = course?.instructor || ''; document.getElementById('f-dsgnr').value = course?.ins_dsgnr || ''; document.getElementById('f-status').value = course?.development_status || 'Not Started'; document.getElementById('f-term').value = course?.term || ''; document.getElementById('f-level').value = course?.level || 0; document.getElementById('f-access').value = course?.accessibility || 0; document.getElementById('f-template').value = course?.template || 'No'; document.getElementById('f-copy').value = course?.Copy_upcoming_flag || 'No'; document.getElementById('f-shell').value = course?.sum_shell_name || ''; document.getElementById('f-notes').value = course?.notes || ''; document.getElementById('f-active').value = (course?.active === false) ? 'false' : 'true'; document.getElementById('modal-title').textContent = course ? "Course Profile" : "Add New Course"; document.getElementById('modal-overlay').classList.add('active'); } function closeModal() { document.getElementById('modal-overlay').classList.remove('active'); } async function saveCourse() { const data = { course_abbrev: document.getElementById('f-abbrev').value, course_number: document.getElementById('f-number').value, instructor: document.getElementById('f-instructor').value, ins_dsgnr: document.getElementById('f-dsgnr').value, development_status: document.getElementById('f-status').value, term: document.getElementById('f-term').value || null, level: parseInt(document.getElementById('f-level').value) || 0, template: document.getElementById('f-template').value, Copy_upcoming_flag: document.getElementById('f-copy').value, sum_shell_name: document.getElementById('f-shell').value, accessibility: parseInt(document.getElementById('f-access').value) || 0, notes: document.getElementById('f-notes').value, active: document.getElementById('f-active').value === 'true' }; try { if (editingId) await api.patch(`/courses?id=eq.${editingId}`, data); else await api.post('/courses', data); showToast(editingId ? "Record Updated" : "Record Created"); closeModal(); loadData(); } catch (err) { showToast("Save Failed"); } } async function deleteCourse(id) { if (confirm('Delete record?')) { try { await api.delete(`/courses?id=eq.${id}`); loadData(); showToast("Record Removed"); } catch (err) { showToast("Delete Failed"); } } } function editCourse(id) { const c = courses.find(x => x.id == id); if (c) openModal(c); } function sortBy(col) { if (sortCol === col) sortAsc = !sortAsc; else { sortCol = col; sortAsc = true; } renderTable(); } function updateSortUI() { const headers = document.querySelectorAll('thead th[onclick]'); headers.forEach(th => { const colAttr = th.getAttribute('onclick').match(/'([^']+)'/)[1]; if (colAttr === sortCol) th.classList.add('active-sort'); else th.classList.remove('active-sort'); }); } async function copyToClipboard(text, el) { try { await navigator.clipboard.writeText(text); el.className = "fa-solid fa-check"; showToast("Copied to Clipboard"); setTimeout(() => el.className = "fa-regular fa-copy", 2000); } catch {} } function exportCSV() { const headers = "Course Prefix,Course #,Instructor,Ins Designer,Program,Term,Development Status,Ally Score,Template,Copied?,Notes\n"; const rows = courses.map(c => `"${c.course_abbrev}","${c.course_number}","${c.instructor}","${c.ins_dsgnr}","${c.online_program}","${c.term}","${c.development_status}","${c.accessibility}","${c.template}","${c.Copy_upcoming_flag}","${c.notes}"`).join("\n"); const blob = new Blob([headers + rows], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'tracker_export.csv'; a.click(); showToast("CSV Export Started"); } window.onload = () => { loadData(); setInterval(() => { document.getElementById('clock').textContent = "Current Time: " + new Date().toLocaleString(); }, 1000); updateLockUI(); };