File: //opt/wpsites/alumni.dataconn.net/app.js
const API = 'api.php';
const state = {
role: null,
currentUser: null,
data: [],
columns: [],
visibleCols: new Set(),
columnFilters: {},
globalSearch: '',
page: 1,
pageSize: 25,
editedRows: new Set(),
hasChanges: false,
isLoggedIn: false
};
const el = (id) => document.getElementById(id);
async function apiCall(action, data = {}) {
const response = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, ...data })
});
return await response.json();
}
function canEdit() {
return state.role === 'Admin' || state.role === 'Editor';
}
function updateRoleUI() {
if (!state.isLoggedIn) {
el('roleBadge').textContent = 'Not Logged In';
el('mainToolbar').classList.add('hidden');
el('mainTable').classList.add('hidden');
el('hintsSection').classList.add('hidden');
el('reportSection').classList.add('hidden');
el('dataPanel').classList.add('hidden');
el('colPanel').classList.add('hidden');
el('exportPanel').classList.add('hidden');
el('reportPanel').classList.add('hidden');
el('welcomeSection').classList.remove('hidden');
return;
}
el('welcomeSection').classList.add('hidden');
el('mainToolbar').classList.remove('hidden');
el('mainTable').classList.remove('hidden');
el('hintsSection').classList.remove('hidden');
el('dataPanel').classList.remove('hidden');
el('colPanel').classList.remove('hidden');
el('exportPanel').classList.remove('hidden');
el('reportPanel').classList.remove('hidden');
el('roleBadge').textContent = state.role;
const isViewer = state.role === 'Viewer';
el('btnReloadDefault').disabled = isViewer;
el('btnAddRecord').disabled = isViewer;
el('btnSaveChanges').disabled = isViewer;
el('btnDownloadLog').disabled = isViewer;
document.querySelectorAll('#colChooser input[type="checkbox"]').forEach(cb => cb.disabled = isViewer);
['selectAllCols', 'clearCols', 'exportSelected', 'copyEmails', 'downloadEmails'].forEach(id => el(id).disabled = isViewer);
}
function markChanged() {
state.hasChanges = true;
el('changeNote').textContent = '⚠ Unsaved changes';
}
function clearChanged() {
state.hasChanges = false;
state.editedRows.clear();
el('changeNote').textContent = '';
}
async function loadData() {
el('dataNote').textContent = 'Loading data...';
const result = await apiCall('getData');
if (result.success) {
state.data = result.data.map((row, idx) => ({ ...row, _rowId: idx }));
initColumns();
renderAll();
clearChanged();
el('dataNote').textContent = 'Data loaded';
} else {
el('dataNote').textContent = 'Error loading data: ' + result.error;
}
}
function initColumns() {
state.columns = Object.keys(state.data[0] || {}).filter(c => c !== '_rowId');
state.visibleCols = new Set(state.columns);
state.columnFilters = Object.fromEntries(state.columns.map(c => [c, '']));
const wrap = el('colChooser');
wrap.innerHTML = '';
state.columns.forEach(col => {
const id = 'col_' + col.replace(/[^a-z0-9]/gi, '_');
const label = document.createElement('label');
label.className = 'col-item';
label.innerHTML = `<input type="checkbox" id="${id}" checked><span>${col}</span>`;
wrap.appendChild(label);
const cb = label.querySelector('input');
cb.disabled = state.role === 'Viewer';
cb.addEventListener('change', (e) => {
if (e.target.checked) state.visibleCols.add(col);
else state.visibleCols.delete(col);
renderTable();
});
});
updateRoleUI();
}
function getFilteredData() {
const visible = [...state.visibleCols];
const glob = state.globalSearch.trim().toLowerCase();
return state.data.filter(row => {
for (const c of state.columns) {
const needle = (state.columnFilters[c] || '').trim().toLowerCase();
if (needle) {
const v = (row[c] ?? '').toString().toLowerCase();
if (!v.includes(needle)) return false;
}
}
if (glob) {
const hit = visible.some(c => ((row[c] ?? '').toString().toLowerCase().includes(glob)));
if (!hit) return false;
}
return true;
});
}
function makeEditable(td, row, col) {
if (!canEdit()) return;
td.className = 'editable-cell';
td.addEventListener('click', () => {
if (td.querySelector('input')) return;
const currentValue = row[col] ?? '';
const input = document.createElement('input');
input.type = 'text';
input.value = currentValue;
td.textContent = '';
td.appendChild(input);
input.focus();
input.select();
const save = async () => {
const newValue = input.value;
const oldValue = row[col];
if (oldValue !== newValue) {
row[col] = newValue;
td.textContent = newValue;
state.editedRows.add(row._rowId);
markChanged();
const tr = td.parentElement;
tr.classList.add('row-edited');
const recordId = row['RecordId'] || row['Record ID'] || row['ID'] || row._rowId;
const name = row['First Name'] || row['Name'] || '';
const lastName = row['Last Name'] || '';
const identifier = name ? `${name} ${lastName}`.trim() : `Record ${recordId}`;
await apiCall('logChange', {
logAction: 'Edit Cell',
logDetails: `${identifier} - Column: "${col}" changed from "${oldValue}" to "${newValue}"`
});
} else {
td.textContent = currentValue;
}
};
input.addEventListener('blur', save);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { save(); }
if (e.key === 'Escape') { td.textContent = currentValue; }
});
});
}
function renderTable() {
const head = el('headerRow');
const filt = el('filterRow');
const cols = [...state.visibleCols];
head.innerHTML = '';
filt.innerHTML = '';
if (canEdit()) {
const thDel = document.createElement('th');
thDel.textContent = 'Actions';
thDel.style.width = '80px';
head.appendChild(thDel);
const tdDel = document.createElement('td');
filt.appendChild(tdDel);
}
cols.forEach(c => {
const th = document.createElement('th');
th.textContent = c;
head.appendChild(th);
const td = document.createElement('td');
td.innerHTML = `<input type="text" placeholder="Filter ${c}" value="${state.columnFilters[c] || ''}">`;
td.querySelector('input').addEventListener('input', (e) => {
state.columnFilters[c] = e.target.value;
state.page = 1;
renderBody();
});
filt.appendChild(td);
});
renderBody();
}
function renderBody() {
const body = el('tableBody');
const data = getFilteredData();
const cols = [...state.visibleCols];
const total = data.length;
const pageSize = state.pageSize;
const pages = Math.max(1, Math.ceil(total / pageSize));
if (state.page > pages) state.page = pages;
const start = (state.page - 1) * pageSize;
const end = Math.min(start + pageSize, total);
body.innerHTML = '';
data.slice(start, end).forEach(r => {
const tr = document.createElement('tr');
if (state.editedRows.has(r._rowId)) tr.classList.add('row-edited');
if (canEdit()) {
const tdDel = document.createElement('td');
const btnDel = document.createElement('button');
btnDel.className = 'btn-delete';
btnDel.textContent = 'Delete';
btnDel.addEventListener('click', async () => {
if (confirm('Delete this record?')) {
const idx = state.data.findIndex(row => row._rowId === r._rowId);
if (idx !== -1) {
const recordId = r['RecordId'] || r['Record ID'] || r['ID'] || r._rowId;
const name = r['First Name'] || r['Name'] || '';
const lastName = r['Last Name'] || '';
const identifier = name ? `${name} ${lastName}`.trim() : `Record ${recordId}`;
const keyCols = ['First Name', 'Last Name', 'Primary Email', 'Graduation Year'].filter(c => state.columns.includes(c));
const summary = keyCols.map(c => `${c}: ${r[c] || 'N/A'}`).join(', ');
state.data.splice(idx, 1);
markChanged();
renderAll();
await apiCall('logChange', {
logAction: 'Delete Record',
logDetails: `Deleted ${identifier} (${summary})`
});
}
}
});
tdDel.appendChild(btnDel);
tr.appendChild(tdDel);
}
cols.forEach(c => {
const td = document.createElement('td');
td.textContent = (r[c] ?? '').toString();
makeEditable(td, r, c);
tr.appendChild(td);
});
body.appendChild(tr);
});
el('pageInfo').textContent = `Rows ${start + 1}-${end} of ${total} (Page ${state.page}/${pages})`;
}
function renderAll() {
renderTable();
updateReportsPreview();
}
function toCSV(rows, cols) {
const esc = (s) => '"' + String(s).replaceAll('"', '""') + '"';
const head = cols.map(esc).join(',');
const body = rows.map(r => cols.map(c => esc(r[c] ?? '')).join(',')).join('\n');
return head + '\n' + body;
}
function download(filename, text) {
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
function exportSelectedColumns() {
const cols = [...state.visibleCols];
const data = getFilteredData();
const csv = toCSV(data, cols);
download('alumni_export.csv', csv);
}
async function saveAllChanges() {
if (!canEdit()) return;
const cols = state.columns;
const cleanData = state.data.map(row => {
const clean = {};
cols.forEach(col => clean[col] = row[col] ?? '');
return clean;
});
const result = await apiCall('saveData', { data: cleanData });
if (result.success) {
alert('Changes saved successfully');
clearChanged();
renderAll();
} else {
alert('Error saving: ' + result.error);
}
}
function showAddModal() {
if (!canEdit()) return;
const form = el('addForm');
form.innerHTML = '';
state.columns.forEach(col => {
const field = document.createElement('div');
field.className = 'form-field';
field.innerHTML = `<label>${col}</label><input type="text" data-col="${col}" />`;
form.appendChild(field);
});
el('addModal').classList.add('active');
}
function hideAddModal() {
el('addModal').classList.remove('active');
}
async function addNewRecord() {
const inputs = el('addForm').querySelectorAll('input');
const newRow = { _rowId: Date.now() };
inputs.forEach(inp => {
const col = inp.dataset.col;
newRow[col] = inp.value;
});
const name = newRow['First Name'] || newRow['Name'] || '';
const lastName = newRow['Last Name'] || '';
const identifier = name ? `${name} ${lastName}`.trim() : 'New Record';
const keyCols = ['First Name', 'Last Name', 'Primary Email', 'Graduation Year'].filter(c => state.columns.includes(c));
const summary = keyCols.map(c => `${c}: ${newRow[c] || 'N/A'}`).join(', ');
state.data.push(newRow);
markChanged();
hideAddModal();
renderAll();
await apiCall('logChange', {
logAction: 'Add Record',
logDetails: `Added ${identifier} (${summary})`
});
}
function collectEmails(mode) {
const primaryKeys = ['Primary Email', 'Primary email', 'primary_email', 'PrimaryEmail'];
const secondaryKeys = ['Secondary Email', 'Secondary email', 'secondary_email', 'SecondaryEmail'];
const allKeys = ['All Emails', 'All emails', 'all_emails', 'AllEmails'];
const colFor = (keys) => keys.find(k => state.columns.includes(k));
const primaryCol = colFor(primaryKeys);
const secondaryCol = colFor(secondaryKeys);
const allCol = colFor(allKeys);
const data = getFilteredData();
const set = new Set();
function addFrom(val) {
if (!val) return;
val.split(/[;,\s]+/).map(s => s.trim()).filter(Boolean).forEach(e => set.add(e));
}
for (const r of data) {
if (mode === 'primary' && primaryCol) addFrom(r[primaryCol]);
else if (mode === 'secondary' && secondaryCol) addFrom(r[secondaryCol]);
else if (mode === 'all') {
if (allCol) addFrom(r[allCol]);
else {
if (primaryCol) addFrom(r[primaryCol]);
if (secondaryCol) addFrom(r[secondaryCol]);
}
}
}
return [...set].join(', ');
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => alert('Emails copied to clipboard')).catch(() => {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
alert('Emails copied (fallback)');
});
}
function updateReportsPreview() {
const out = el('reportOut');
out.innerHTML = '';
el('reportSection').classList.add('hidden');
}
function countBy(col) {
const map = new Map();
for (const r of state.data) {
const k = (r[col] ?? '').toString().trim();
if (!k) continue;
map.set(k, (map.get(k) || 0) + 1);
}
return [...map.entries()].sort((a, b) => b[1] - a[1]);
}
function runReport() {
const rpt = el('reportSelect').value;
if (!rpt) return;
const out = el('reportOut');
out.innerHTML = '';
el('reportSection').classList.remove('hidden');
const makeCard = (label, metric) => {
const d = document.createElement('div');
d.className = 'card';
d.innerHTML = `<div class="label" title="${label}">${label}</div><div class="metric">${metric}</div>`;
return d;
};
if (state.data.length === 0) {
out.textContent = 'No data loaded.';
return;
}
const yearKeys = ['Graduation Year', 'Graduation year', 'graduation_year', 'Year'];
const degreeKeys = ['Degree'];
const concKeys = ['Concentration (Atomic)', 'Concentration (Standardized)', 'Concentration', 'Major/Concentration'];
const stateKeys = ['State'];
const pick = (keys) => keys.find(k => state.columns.includes(k));
if (rpt === 'gradYear') {
const col = pick(yearKeys);
if (!col) { out.textContent = 'Graduation Year column not found.'; return; }
const pairs = countBy(col).slice(0, 12);
pairs.forEach(([k, v]) => out.appendChild(makeCard(k, v)));
} else if (rpt === 'degree') {
const col = pick(degreeKeys);
if (!col) { out.textContent = 'Degree column not found.'; return; }
const pairs = countBy(col).slice(0, 12);
pairs.forEach(([k, v]) => out.appendChild(makeCard(k, v)));
} else if (rpt === 'concentration') {
const col = pick(concKeys);
if (!col) { out.textContent = 'A concentration column was not found.'; return; }
const pairs = countBy(col).slice(0, 12);
pairs.forEach(([k, v]) => out.appendChild(makeCard(k, v)));
} else if (rpt === 'state') {
const col = pick(stateKeys);
if (!col) { out.textContent = 'State column not found.'; return; }
const pairs = countBy(col).slice(0, 12);
pairs.forEach(([k, v]) => out.appendChild(makeCard(k, v)));
}
}
async function downloadChangeLog() {
const result = await apiCall('getChangeLog');
if (result.success) {
if (result.log.length === 0) {
alert('No changes logged yet.');
return;
}
let logText = 'ALUMNI DATABASE CHANGE LOG\n';
logText += '='.repeat(60) + '\n\n';
result.log.forEach((entry, idx) => {
logText += `[${idx + 1}] ${entry.timestamp}\n`;
logText += `User: ${entry.user} (${entry.role})\n`;
logText += `Action: ${entry.action}\n`;
logText += `Details: ${entry.details}\n`;
logText += '-'.repeat(60) + '\n\n';
});
download('change_log.txt', logText);
} else {
alert('Error loading log: ' + result.error);
}
}
// Event Listeners
el('btnLogin').addEventListener('click', async () => {
const username = el('loginUser').value.trim();
const password = el('loginPass').value;
const result = await apiCall('login', { username, password });
if (result.success) {
state.role = result.role;
state.currentUser = username;
state.isLoggedIn = true;
updateRoleUI();
await loadData();
alert(`Signed in as ${state.role}`);
} else {
alert('Invalid credentials.');
}
});
el('btnLogout').addEventListener('click', async () => {
await apiCall('logout');
location.reload();
});
el('btnReloadDefault').addEventListener('click', async () => {
if (state.hasChanges && !confirm('You have unsaved changes. Reload anyway?')) return;
await loadData();
});
el('btnAddRecord').addEventListener('click', showAddModal);
el('btnSaveChanges').addEventListener('click', saveAllChanges);
el('cancelAdd').addEventListener('click', hideAddModal);
el('confirmAdd').addEventListener('click', addNewRecord);
el('btnDownloadLog').addEventListener('click', downloadChangeLog);
['selectAllCols', 'clearCols'].forEach(id => el(id).addEventListener('click', () => {
if (state.role === 'Viewer') return;
if (id === 'selectAllCols') {
state.columns.forEach(c => state.visibleCols.add(c));
document.querySelectorAll('#colChooser input[type="checkbox"]').forEach(cb => cb.checked = true);
}
if (id === 'clearCols') {
state.visibleCols.clear();
document.querySelectorAll('#colChooser input[type="checkbox"]').forEach(cb => cb.checked = false);
}
renderTable();
}));
el('globalSearch').addEventListener('input', (e) => {
state.globalSearch = e.target.value;
state.page = 1;
renderBody();
});
el('clearSearch').addEventListener('click', () => {
el('globalSearch').value = '';
state.globalSearch = '';
renderBody();
});
el('pageSize').addEventListener('change', (e) => {
state.pageSize = parseInt(e.target.value, 10) || 25;
state.page = 1;
renderBody();
});
el('prevPage').addEventListener('click', () => {
state.page = Math.max(1, state.page - 1);
renderBody();
});
el('nextPage').addEventListener('click', () => {
const total = getFilteredData().length;
const pages = Math.max(1, Math.ceil(total / state.pageSize));
state.page = Math.min(pages, state.page + 1);
renderBody();
});
el('exportSelected').addEventListener('click', () => {
if (state.role === 'Viewer') return;
exportSelectedColumns();
});
el('copyEmails').addEventListener('click', () => {
if (state.role === 'Viewer') return;
const mode = el('emailMode').value;
const s = collectEmails(mode);
copyToClipboard(s);
});
el('downloadEmails').addEventListener('click', () => {
if (state.role === 'Viewer') return;
const mode = el('emailMode').value;
const s = collectEmails(mode);
download('emails.txt', s);
});
el('runReport').addEventListener('click', runReport);
el('clearReport').addEventListener('click', () => {
el('reportOut').innerHTML = '';
el('reportSection').classList.add('hidden');
el('reportSelect').value = '';
});
// Check session on load
(async () => {
const result = await apiCall('checkSession');
if (result.success && result.loggedIn) {
state.role = result.role;
state.currentUser = result.username;
state.isLoggedIn = true;
updateRoleUI();
await loadData();
} else {
updateRoleUI();
}
})();