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(); };