HEX
Server: nginx/1.24.0
System: Linux webserver 6.8.0-85-generic #85-Ubuntu SMP PREEMPT_DYNAMIC Thu Sep 18 15:26:59 UTC 2025 x86_64
User: wpuser (1002)
PHP: 8.3.6
Disabled: NONE
Upload Files
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();
  }
})();