import './admin.css'; import { initAuth, onAuthChange, loginWithGoogle, logout } from './auth.js'; import { db } from './firebase.js'; import { doc, getDoc, setDoc } from 'firebase/firestore'; // Hardcoded admin allowlist — used to bootstrap the Firestore config/admins document // on first login if it doesn't exist yet. const SEED_ADMINS = ['pcgurudm@gmail.com']; import { getDonors, addDonorWithDate, addDonorsBatch, updateDonor, deleteDonor, calculateTotal, } from './donors.js'; let allDonors = []; // ===== Init ===== document.addEventListener('DOMContentLoaded', () => { initAuth(); initAuthUI(); initTabs(); initAddForm(); initCSVImport(); initEditModal(); }); // ===== Auth ===== function initAuthUI() { document.getElementById('loginGoogle').addEventListener('click', loginWithGoogle); document.getElementById('logoutBtn').addEventListener('click', logout); document.getElementById('unauthorizedLogout').addEventListener('click', logout); onAuthChange(handleAuth); } async function handleAuth(user) { const authSection = document.getElementById('authSection'); const unauthorizedSection = document.getElementById('unauthorizedSection'); const adminContent = document.getElementById('adminContent'); const logoutBtn = document.getElementById('logoutBtn'); if (!user) { authSection.classList.remove('hidden'); unauthorizedSection.classList.add('hidden'); adminContent.classList.add('hidden'); logoutBtn.style.display = 'none'; return; } logoutBtn.style.display = ''; // Check admin allowlist const isAdmin = await checkAdmin(user.email); if (!isAdmin) { authSection.classList.add('hidden'); unauthorizedSection.classList.remove('hidden'); adminContent.classList.add('hidden'); return; } authSection.classList.add('hidden'); unauthorizedSection.classList.add('hidden'); adminContent.classList.remove('hidden'); loadDonors(); } async function checkAdmin(email) { try { const adminDocRef = doc(db, 'config', 'admins'); const adminDoc = await getDoc(adminDocRef); if (!adminDoc.exists()) { // No allowlist yet — bootstrap it if this user is in the seed list if (SEED_ADMINS.includes(email)) { await setDoc(adminDocRef, { emails: SEED_ADMINS }); console.log('Admin allowlist created with seed admins.'); return true; } // No doc and not a seed admin — deny return false; } const emails = adminDoc.data()?.emails || []; return emails.includes(email); } catch (err) { console.error('Admin check failed:', err); return false; } } // ===== Tabs ===== function initTabs() { document.querySelectorAll('.admin-tab').forEach((btn) => { btn.addEventListener('click', () => { document.querySelectorAll('.admin-tab').forEach((b) => b.classList.remove('active')); document.querySelectorAll('.admin-panel').forEach((p) => p.classList.remove('active')); btn.classList.add('active'); document.getElementById(`panel-${btn.dataset.tab}`).classList.add('active'); }); }); } // ===== Load Donors ===== async function loadDonors() { try { allDonors = await getDonors(); updateStats(); renderDonorTable(); } catch (err) { console.error('Failed to load donors:', err); showToast('Failed to load donors.', 'error'); } } function updateStats() { const total = calculateTotal(allDonors); document.getElementById('statTotal').textContent = `$${total.toLocaleString()}`; document.getElementById('statCount').textContent = allDonors.length; } // ===== Add Donor Form ===== function initAddForm() { const form = document.getElementById('addDonorForm'); form.addEventListener('submit', async (e) => { e.preventDefault(); const btn = document.getElementById('addDonorBtn'); btn.disabled = true; btn.textContent = 'Adding...'; try { await addDonorWithDate({ name: document.getElementById('donorName').value.trim(), amount: document.getElementById('donorAmount').value, classYear: document.getElementById('donorClassYear').value.trim(), message: document.getElementById('donorMessage').value.trim(), anonymous: document.getElementById('donorAnonymous').checked, date: document.getElementById('donorDate').value || null, }); showToast('Donor added!', 'success'); form.reset(); await loadDonors(); } catch (err) { console.error('Failed to add donor:', err); showToast('Failed to add donor.', 'error'); } finally { btn.disabled = false; btn.textContent = 'Add Donor'; } }); } // ===== CSV Import ===== let csvRows = []; let csvHeaders = []; function initCSVImport() { const fileInput = document.getElementById('csvFile'); fileInput.addEventListener('change', handleFileSelect); document.getElementById('importBtn').addEventListener('click', runImport); document.getElementById('csvCancelBtn').addEventListener('click', resetCSV); // Update preview when mappings change document.querySelectorAll('.mapping-grid select').forEach((sel) => { sel.addEventListener('change', updatePreview); }); } function handleFileSelect(e) { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (evt) => { const text = evt.target.result; const parsed = parseCSV(text); if (parsed.length < 2) { showToast('CSV file appears empty or has no data rows.', 'error'); return; } csvHeaders = parsed[0]; csvRows = parsed.slice(1); setupMapping(); document.getElementById('csvMapping').classList.remove('hidden'); }; reader.readAsText(file); } function parseCSV(text) { const rows = []; let current = []; let field = ''; let inQuotes = false; for (let i = 0; i < text.length; i++) { const ch = text[i]; const next = text[i + 1]; if (inQuotes) { if (ch === '"' && next === '"') { field += '"'; i++; } else if (ch === '"') { inQuotes = false; } else { field += ch; } } else { if (ch === '"') { inQuotes = true; } else if (ch === ',') { current.push(field.trim()); field = ''; } else if (ch === '\n' || (ch === '\r' && next === '\n')) { current.push(field.trim()); if (current.some((c) => c !== '')) rows.push(current); current = []; field = ''; if (ch === '\r') i++; } else { field += ch; } } } // Last field/row current.push(field.trim()); if (current.some((c) => c !== '')) rows.push(current); return rows; } function setupMapping() { const fields = ['mapName', 'mapAmount', 'mapClassYear', 'mapMessage', 'mapDate', 'mapAnonymous']; const hints = { mapName: ['name', 'donor', 'first', 'full'], mapAmount: ['amount', 'donation', 'gift', 'total', 'sum'], mapClassYear: ['class', 'year', 'grad'], mapMessage: ['message', 'note', 'comment'], mapDate: ['date', 'time', 'when'], mapAnonymous: ['anonymous', 'anon'], }; for (const fieldId of fields) { const select = document.getElementById(fieldId); select.innerHTML = ''; csvHeaders.forEach((h, i) => { const opt = document.createElement('option'); opt.value = i; opt.textContent = h; select.appendChild(opt); }); // Auto-map by fuzzy match const keywords = hints[fieldId]; const match = csvHeaders.findIndex((h) => keywords.some((k) => h.toLowerCase().includes(k)) ); if (match !== -1) select.value = match; } updatePreview(); } function getMappedRow(row) { const get = (id) => { const val = document.getElementById(id).value; return val !== '' ? (row[Number(val)] || '') : ''; }; return { name: get('mapName'), amount: get('mapAmount'), classYear: get('mapClassYear'), message: get('mapMessage'), date: get('mapDate'), anonymous: get('mapAnonymous'), }; } function updatePreview() { const head = document.getElementById('csvPreviewHead'); const body = document.getElementById('csvPreviewBody'); head.innerHTML = '