<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Prism Digital — Lead Tracker</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet" />
<style>
/* ── CONFIG ── fill these in after Google Cloud setup ── */
/* See: PRISM_API_KEY and PRISM_SHEET_ID in the <script> below */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0d0f12;
--surface: #13161b;
--surface2: #1a1e25;
--border: rgba(255,255,255,0.07);
--border-hover: rgba(255,255,255,0.14);
--text: #e8eaf0;
--text-muted: #6b7280;
--text-dim: #3d4452;
--accent: #3b82f6;
--accent-glow: rgba(59,130,246,0.15);
--accent-dim: rgba(59,130,246,0.25);
--green: #10b981;
--green-dim: rgba(16,185,129,0.15);
--amber: #f59e0b;
--amber-dim: rgba(245,158,11,0.15);
--red: #ef4444;
--red-dim: rgba(239,68,68,0.15);
--teal: #14b8a6;
--teal-dim: rgba(20,184,166,0.15);
--font: 'DM Sans', sans-serif;
--mono: 'DM Mono', monospace;
--radius: 10px;
--radius-lg: 16px;
}
html, body {
background: var(--bg);
color: var(--text);
font-family: var(--font);
font-size: 14px;
min-height: 100vh;
line-height: 1.6;
}
/* ── PASSWORD GATE ── */
#gate {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.gate-card {
width: 100%;
max-width: 360px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 2.5rem 2rem;
text-align: center;
}
.gate-logo {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-bottom: 2rem;
}
.gate-prism {
width: 32px;
height: 32px;
}
.gate-brand {
font-size: 18px;
font-weight: 500;
letter-spacing: -0.02em;
}
.gate-sub {
font-size: 12px;
color: var(--text-muted);
margin-bottom: 1.75rem;
}
.gate-label {
font-size: 12px;
color: var(--text-muted);
text-align: left;
margin-bottom: 6px;
display: block;
}
.gate-input {
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-family: var(--mono);
font-size: 14px;
padding: 10px 14px;
outline: none;
margin-bottom: 12px;
transition: border-color 0.15s;
}
.gate-input:focus { border-color: var(--accent); }
.gate-input.error { border-color: var(--red); }
.gate-btn {
width: 100%;
background: var(--accent);
color: #fff;
border: none;
border-radius: var(--radius);
font-family: var(--font);
font-size: 14px;
font-weight: 500;
padding: 10px;
cursor: pointer;
transition: opacity 0.15s, transform 0.1s;
}
.gate-btn:hover { opacity: 0.9; }
.gate-btn:active { transform: scale(0.98); }
.gate-error {
font-size: 12px;
color: var(--red);
margin-top: 8px;
min-height: 18px;
}
/* ── MAIN APP ── */
#app { display: none; min-height: 100vh; }
header {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 0 2rem;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 10;
}
.header-left {
display: flex;
align-items: center;
gap: 10px;
}
.header-brand { font-size: 15px; font-weight: 500; letter-spacing: -0.02em; }
.header-sep { color: var(--text-dim); }
.header-page { font-size: 14px; color: var(--text-muted); }
.header-right {
display: flex;
align-items: center;
gap: 10px;
}
.sync-indicator {
font-size: 11px;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 5px;
}
.sync-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--green);
}
.sync-dot.syncing { background: var(--amber); animation: pulse 1s infinite; }
.sync-dot.error { background: var(--red); }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
.logout-btn {
background: none;
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-muted);
font-family: var(--font);
font-size: 12px;
padding: 5px 12px;
cursor: pointer;
transition: all 0.15s;
}
.logout-btn:hover { border-color: var(--border-hover); color: var(--text); }
main { padding: 2rem; max-width: 1100px; margin: 0 auto; }
/* stats */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 1.75rem;
}
.stat {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem 1.25rem;
}
.stat-val {
font-size: 28px;
font-weight: 300;
letter-spacing: -0.03em;
line-height: 1;
margin-bottom: 4px;
}
.stat-key {
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
}
/* toolbar */
.toolbar {
display: flex;
gap: 8px;
margin-bottom: 1.25rem;
flex-wrap: wrap;
align-items: center;
}
.tb-input {
flex: 1;
min-width: 180px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-family: var(--font);
font-size: 13px;
padding: 8px 12px;
outline: none;
transition: border-color 0.15s;
}
.tb-input:focus { border-color: var(--accent); }
.tb-input::placeholder { color: var(--text-dim); }
.tb-select {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-family: var(--font);
font-size: 13px;
padding: 8px 12px;
outline: none;
cursor: pointer;
}
.tb-select:focus { border-color: var(--accent); }
.btn {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-family: var(--font);
font-size: 13px;
font-weight: 500;
padding: 8px 16px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
transition: all 0.15s;
}
.btn:hover { border-color: var(--border-hover); background: var(--surface2); }
.btn:active { transform: scale(0.98); }
.btn-accent {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.btn-accent:hover { background: #2563eb; border-color: #2563eb; }
/* table */
.table-wrap {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
thead th {
padding: 10px 14px;
font-size: 11px;
font-weight: 500;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
text-align: left;
border-bottom: 1px solid var(--border);
cursor: pointer;
user-select: none;
white-space: nowrap;
}
thead th:hover { color: var(--text); }
tbody td {
padding: 11px 14px;
border-bottom: 1px solid var(--border);
font-size: 13px;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: var(--surface2); }
.badge {
display: inline-block;
padding: 2px 9px;
border-radius: 99px;
font-size: 11px;
font-weight: 500;
}
.badge-discovery { background: var(--accent-dim); color: #93c5fd; }
.badge-proposal { background: var(--amber-dim); color: #fcd34d; }
.badge-progress { background: var(--teal-dim); color: #5eead4; }
.badge-launched { background: var(--green-dim); color: #6ee7b7; }
.badge-lost { background: rgba(255,255,255,0.05); color: var(--text-muted); }
.act-btn {
background: none;
border: none;
color: var(--text-dim);
cursor: pointer;
padding: 3px 5px;
border-radius: 6px;
font-size: 14px;
line-height: 1;
transition: all 0.12s;
}
.act-btn:hover { background: var(--surface2); color: var(--text); }
.empty {
text-align: center;
padding: 3rem 1rem;
color: var(--text-muted);
font-size: 13px;
}
/* modal */
.overlay {
display: none;
position: fixed; inset: 0;
background: rgba(0,0,0,0.6);
z-index: 50;
align-items: center;
justify-content: center;
padding: 1rem;
backdrop-filter: blur(3px);
}
.overlay.open { display: flex; }
.modal {
background: var(--surface);
border: 1px solid var(--border-hover);
border-radius: var(--radius-lg);
padding: 1.5rem;
width: 100%;
max-width: 440px;
max-height: 90vh;
overflow-y: auto;
}
.modal-head {
font-size: 15px;
font-weight: 500;
margin-bottom: 1.25rem;
letter-spacing: -0.02em;
}
.fg { margin-bottom: 12px; }
.fg label {
display: block;
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 5px;
}
.fg input, .fg select, .fg textarea {
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-family: var(--font);
font-size: 13px;
padding: 8px 12px;
outline: none;
transition: border-color 0.15s;
}
.fg input:focus, .fg select:focus, .fg textarea:focus { border-color: var(--accent); }
.fg textarea { height: 80px; resize: vertical; }
.fg-row { display: flex; gap: 10px; }
.fg-row .fg { flex: 1; }
.modal-foot {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 1.25rem;
}
.note-item {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 10px 12px;
margin-bottom: 8px;
}
.note-date { font-size: 11px; color: var(--text-muted); margin-bottom: 3px; font-family: var(--mono); }
.note-text { font-size: 13px; }
.spinner {
display: inline-block;
width: 14px; height: 14px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.6s linear infinite;
vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }
.toast {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
background: var(--surface2);
border: 1px solid var(--border-hover);
border-radius: var(--radius);
padding: 10px 16px;
font-size: 13px;
color: var(--text);
z-index: 200;
opacity: 0;
transform: translateY(8px);
transition: all 0.2s;
pointer-events: none;
}
.toast.show { opacity: 1; transform: translateY(0); }
@media (max-width: 640px) {
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.col-contact, .col-service, .col-date { display: none; }
main { padding: 1rem; }
header { padding: 0 1rem; }
}
</style>
</head>
<body>
<!-- ── PASSWORD GATE ── -->
<div id="gate">
<div class="gate-card">
<div class="gate-logo">
<svg class="gate-prism" viewBox="0 0 32 32" fill="none">
<polygon points="16,2 30,28 2,28" fill="none" stroke="#3b82f6" stroke-width="1.5"/>
<line x1="16" y1="2" x2="16" y2="28" stroke="#3b82f6" stroke-width="0.75" stroke-opacity="0.5"/>
<line x1="16" y1="2" x2="2" y2="28" stroke="#3b82f6" stroke-width="0.75" stroke-opacity="0.3"/>
<line x1="9" y1="15" x2="30" y2="28" stroke="#3b82f6" stroke-width="0.75" stroke-opacity="0.3"/>
</svg>
<span class="gate-brand">Prism Digital</span>
</div>
<div class="gate-sub">Lead Tracker — private dashboard</div>
<label class="gate-label">Password</label>
<input class="gate-input" id="gate-pw" type="password" placeholder="Enter password" onkeydown="if(event.key==='Enter')unlock()" />
<button class="gate-btn" onclick="unlock()">Unlock dashboard</button>
<div class="gate-error" id="gate-err"></div>
</div>
</div>
<!-- ── MAIN APP ── -->
<div id="app">
<header>
<div class="header-left">
<svg width="22" height="22" viewBox="0 0 32 32" fill="none">
<polygon points="16,2 30,28 2,28" fill="none" stroke="#3b82f6" stroke-width="1.5"/>
<line x1="16" y1="2" x2="16" y2="28" stroke="#3b82f6" stroke-width="0.75" stroke-opacity="0.5"/>
</svg>
<span class="header-brand">Prism Digital</span>
<span class="header-sep">/</span>
<span class="header-page">Lead Tracker</span>
</div>
<div class="header-right">
<div class="sync-indicator">
<div class="sync-dot" id="sync-dot"></div>
<span id="sync-label">Connected</span>
</div>
<button class="logout-btn" onclick="logout()">Lock</button>
</div>
</header>
<main>
<div class="stats-grid" id="stats-grid"></div>
<div class="toolbar">
<input class="tb-input" type="text" id="search" placeholder="Search leads…" oninput="renderTable()" />
<select class="tb-select" id="filter-stage" onchange="renderTable()">
<option value="">All stages</option>
<option>Discovery</option><option>Proposal</option>
<option>In Progress</option><option>Launched</option><option>Lost</option>
</select>
<select class="tb-select" id="filter-service" onchange="renderTable()">
<option value="">All services</option>
<option>Website Design</option><option>Content Development</option>
<option>SEO</option><option>Hosting</option>
<option>Project Management</option><option>Other</option>
</select>
<button class="btn btn-accent" onclick="openAdd()">+ Add lead</button>
</div>
<div class="table-wrap">
<div id="loading" style="text-align:center;padding:2.5rem;color:var(--text-muted);font-size:13px;">
<span class="spinner"></span> Loading from Google Sheets…
</div>
<table id="leads-table" style="display:none;">
<thead>
<tr>
<th style="width:22%" onclick="sort('name')">Name <span id="s-name"></span></th>
<th class="col-contact" style="width:19%" onclick="sort('contact')">Contact <span id="s-contact"></span></th>
<th class="col-service" style="width:16%" onclick="sort('service')">Service <span id="s-service"></span></th>
<th style="width:15%" onclick="sort('stage')">Stage <span id="s-stage"></span></th>
<th class="col-date" style="width:13%" onclick="sort('added')">Added <span id="s-added"></span></th>
<th style="width:15%"></th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
<div id="empty" class="empty" style="display:none;">No leads found.</div>
</div>
</main>
</div>
<!-- Add / Edit modal -->
<div class="overlay" id="lead-modal">
<div class="modal">
<div class="modal-head" id="modal-title">Add new lead</div>
<div class="fg">
<label>Client / Business name *</label>
<input type="text" id="f-name" placeholder="Acme Studio" />
</div>
<div class="fg-row">
<div class="fg">
<label>Contact name</label>
<input type="text" id="f-contact" placeholder="Jane Smith" />
</div>
<div class="fg">
<label>Email</label>
<input type="text" id="f-email" placeholder="jane@acme.com" />
</div>
</div>
<div class="fg-row">
<div class="fg">
<label>Service interest</label>
<select id="f-service">
<option value="">— select —</option>
<option>Website Design</option><option>Content Development</option>
<option>SEO</option><option>Hosting</option>
<option>Project Management</option><option>Other</option>
</select>
</div>
<div class="fg">
<label>Pipeline stage</label>
<select id="f-stage">
<option>Discovery</option><option>Proposal</option>
<option>In Progress</option><option>Launched</option><option>Lost</option>
</select>
</div>
</div>
<div class="fg">
<label>Notes</label>
<textarea id="f-notes" placeholder="Any context about this lead…"></textarea>
</div>
<div class="modal-foot">
<button class="btn" onclick="closeModal('lead-modal')">Cancel</button>
<button class="btn btn-accent" id="save-btn" onclick="saveLead()">Save lead</button>
</div>
</div>
</div>
<!-- Notes modal -->
<div class="overlay" id="notes-modal">
<div class="modal">
<div class="modal-head" id="notes-title">Notes</div>
<div id="notes-list"></div>
<div class="fg" style="margin-top:10px;">
<label>Add a note</label>
<textarea id="new-note" placeholder="Log an update, call, or follow-up…"></textarea>
</div>
<div class="modal-foot">
<button class="btn" onclick="closeModal('notes-modal')">Close</button>
<button class="btn btn-accent" onclick="addNote()">Add note</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
// ══════════════════════════════════════════════
// CONFIGURATION — fill these in
// ══════════════════════════════════════════════
const CONFIG = {
PASSWORD: 'prism2024', // ← change this to your password
API_KEY: 'AIzaSyCHhE3vwo85ED7TLWrrKoODsFS0sjYK_iA', // ← paste your Google API key
SHEET_ID: '1IyXLl_Pw_0wM1bv9qrIzYT1zoeaHd9AW4GwTYaAw6Rs', // ← paste your Google Sheet ID
SHEET_NAME: 'Leads', // ← sheet tab name (keep as "Leads")
};
// ══════════════════════════════════════════════
const BASE = `https://sheets.googleapis.com/v4/spreadsheets/${CONFIG.SHEET_ID}/values`;
const HEADERS = ['ID','Name','Contact','Email','Service','Stage','Added','Notes'];
let leads = [];
let editRow = null; // 1-based sheet row of lead being edited
let notesRow = null;
let sortCol = 'added';
let sortDir = 'desc';
let authenticated = false;
// ── AUTH ──────────────────────────────────────
function unlock() {
const pw = document.getElementById('gate-pw').value;
const err = document.getElementById('gate-err');
if (pw === CONFIG.PASSWORD) {
authenticated = true;
document.getElementById('gate').style.display = 'none';
document.getElementById('app').style.display = 'block';
loadLeads();
} else {
err.textContent = 'Incorrect password.';
document.getElementById('gate-pw').classList.add('error');
setTimeout(() => {
err.textContent = '';
document.getElementById('gate-pw').classList.remove('error');
}, 2000);
}
}
function logout() {
authenticated = false;
document.getElementById('app').style.display = 'none';
document.getElementById('gate').style.display = 'flex';
document.getElementById('gate-pw').value = '';
}
// ── SYNC STATUS ───────────────────────────────
function setSyncState(state) {
const dot = document.getElementById('sync-dot');
const lbl = document.getElementById('sync-label');
dot.className = 'sync-dot' + (state === 'syncing' ? ' syncing' : state === 'error' ? ' error' : '');
lbl.textContent = state === 'syncing' ? 'Saving…' : state === 'error' ? 'Sync error' : 'Synced';
}
// ── GOOGLE SHEETS API ─────────────────────────
async function sheetsGet(range) {
const url = `${BASE}/${encodeURIComponent(range)}?key=${CONFIG.API_KEY}`;
const r = await fetch(url);
if (!r.ok) throw new Error('Sheets read failed: ' + r.status);
return r.json();
}
async function sheetsAppend(values) {
const url = `${BASE}/${encodeURIComponent(CONFIG.SHEET_NAME)}:append?valueInputOption=RAW&insertDataOption=INSERT_ROWS&key=${CONFIG.API_KEY}`;
const r = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ values })
});
if (!r.ok) throw new Error('Sheets append failed: ' + r.status);
return r.json();
}
async function sheetsUpdate(range, values) {
const url = `${BASE}/${encodeURIComponent(range)}?valueInputOption=RAW&key=${CONFIG.API_KEY}`;
const r = await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ values })
});
if (!r.ok) throw new Error('Sheets update failed: ' + r.status);
return r.json();
}
async function sheetsClear(range) {
const url = `${BASE}/${encodeURIComponent(range)}:clear?key=${CONFIG.API_KEY}`;
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' } });
if (!r.ok) throw new Error('Sheets clear failed: ' + r.status);
return r.json();
}
// ── LOAD ──────────────────────────────────────
async function loadLeads() {
setSyncState('syncing');
try {
const data = await sheetsGet(`${CONFIG.SHEET_NAME}!A:H`);
const rows = data.values || [];
// ensure header row
if (rows.length === 0 || rows[0][0] !== 'ID') {
await sheetsAppend([HEADERS]);
leads = [];
} else {
leads = rows.slice(1).map((r, i) => ({
id: r[0] || '',
name: r[1] || '',
contact: r[2] || '',
email: r[3] || '',
service: r[4] || '',
stage: r[5] || 'Discovery',
added: r[6] || '',
notes: r[7] || '',
_row: i + 2 // 1-based sheet row (row 1 = headers)
})).filter(l => l.id);
}
setSyncState('ok');
} catch(e) {
setSyncState('error');
showToast('Could not load from Google Sheets. Check your API key and Sheet ID.');
leads = [];
}
document.getElementById('loading').style.display = 'none';
document.getElementById('leads-table').style.display = '';
renderAll();
}
// ── SAVE LEAD ─────────────────────────────────
async function saveLead() {
const name = document.getElementById('f-name').value.trim();
if (!name) { document.getElementById('f-name').focus(); return; }
const row = [
editRow ? leads.find(l => l._row === editRow)?.id || uid() : uid(),
name,
document.getElementById('f-contact').value.trim(),
document.getElementById('f-email').value.trim(),
document.getElementById('f-service').value,
document.getElementById('f-stage').value,
editRow ? leads.find(l => l._row === editRow)?.added || today() : today(),
document.getElementById('f-notes').value.trim()
];
setSyncState('syncing');
try {
if (editRow) {
await sheetsUpdate(`${CONFIG.SHEET_NAME}!A${editRow}:H${editRow}`, [row]);
showToast('Lead updated.');
} else {
await sheetsAppend([row]);
showToast('Lead added.');
}
await loadLeads();
} catch(e) {
setSyncState('error');
showToast('Save failed — check console.');
}
closeModal('lead-modal');
}
// ── DELETE ────────────────────────────────────
async function deleteLead(row) {
if (!confirm('Delete this lead?')) return;
setSyncState('syncing');
try {
await sheetsClear(`${CONFIG.SHEET_NAME}!A${row}:H${row}`);
showToast('Lead removed.');
await loadLeads();
} catch(e) {
setSyncState('error');
showToast('Delete failed.');
}
}
// ── NOTES ─────────────────────────────────────
function openNotes(row) {
notesRow = row;
const lead = leads.find(l => l._row === row);
document.getElementById('notes-title').textContent = (lead?.name || 'Lead') + ' — notes';
document.getElementById('new-note').value = '';
renderNotesList(lead?.notes || '');
document.getElementById('notes-modal').classList.add('open');
}
function renderNotesList(raw) {
const el = document.getElementById('notes-list');
const entries = raw ? raw.split('|||').filter(Boolean) : [];
if (!entries.length) {
el.innerHTML = '<div style="color:var(--text-muted);font-size:12px;padding:4px 0 8px;">No notes yet.</div>';
return;
}
el.innerHTML = [...entries].reverse().map(e => {
const sep = e.indexOf(': ');
const date = sep > -1 ? e.slice(0, sep) : '';
const text = sep > -1 ? e.slice(sep + 2) : e;
return `<div class="note-item"><div class="note-date">${date}</div><div class="note-text">${text}</div></div>`;
}).join('');
}
async function addNote() {
const text = document.getElementById('new-note').value.trim();
if (!text) return;
const lead = leads.find(l => l._row === notesRow);
if (!lead) return;
const existing = lead.notes || '';
const entry = `${today()}: ${text}`;
const updated = existing ? existing + '|||' + entry : entry;
setSyncState('syncing');
try {
await sheetsUpdate(`${CONFIG.SHEET_NAME}!H${notesRow}`, [[updated]]);
document.getElementById('new-note').value = '';
await loadLeads();
const refreshed = leads.find(l => l.id === lead.id);
renderNotesList(refreshed?.notes || '');
showToast('Note added.');
} catch(e) {
setSyncState('error');
showToast('Note save failed.');
}
}
// ── MODALS ────────────────────────────────────
function openAdd() {
editRow = null;
document.getElementById('modal-title').textContent = 'Add new lead';
document.getElementById('save-btn').textContent = 'Save lead';
['f-name','f-contact','f-email','f-notes'].forEach(id => document.getElementById(id).value = '');
document.getElementById('f-service').value = '';
document.getElementById('f-stage').value = 'Discovery';
document.getElementById('lead-modal').classList.add('open');
}
function openEdit(row) {
const lead = leads.find(l => l._row === row);
if (!lead) return;
editRow = row;
document.getElementById('modal-title').textContent = 'Edit lead';
document.getElementById('save-btn').textContent = 'Save changes';
document.getElementById('f-name').value = lead.name;
document.getElementById('f-contact').value = lead.contact;
document.getElementById('f-email').value = lead.email;
document.getElementById('f-service').value = lead.service;
document.getElementById('f-stage').value = lead.stage;
document.getElementById('f-notes').value = '';
document.getElementById('lead-modal').classList.add('open');
}
function closeModal(id) {
document.getElementById(id).classList.remove('open');
}
// ── RENDER ────────────────────────────────────
function renderStats() {
const total = leads.length;
const active = leads.filter(l => !['Launched','Lost'].includes(l.stage)).length;
const proposals = leads.filter(l => l.stage === 'Proposal').length;
const launched = leads.filter(l => l.stage === 'Launched').length;
document.getElementById('stats-grid').innerHTML = [
[total, 'Total leads'],
[active, 'Active'],
[proposals, 'Proposals'],
[launched, 'Launched']
].map(([v,k]) => `<div class="stat"><div class="stat-val">${v}</div><div class="stat-key">${k}</div></div>`).join('');
}
function getFiltered() {
const q = document.getElementById('search').value.toLowerCase();
const st = document.getElementById('filter-stage').value;
const sv = document.getElementById('filter-service').value;
return leads.filter(l => {
const mq = !q || [l.name,l.contact,l.email,l.service].some(v => v?.toLowerCase().includes(q));
return mq && (!st || l.stage === st) && (!sv || l.service === sv);
}).sort((a, b) => {
let av = a[sortCol] || '', bv = b[sortCol] || '';
const cmp = av < bv ? -1 : av > bv ? 1 : 0;
return sortDir === 'asc' ? cmp : -cmp;
});
}
function sort(col) {
if (sortCol === col) sortDir = sortDir === 'asc' ? 'desc' : 'asc';
else { sortCol = col; sortDir = 'asc'; }
['name','contact','service','stage','added'].forEach(c => {
const el = document.getElementById('s-' + c);
if (el) el.textContent = '';
});
const el = document.getElementById('s-' + col);
if (el) el.textContent = sortDir === 'asc' ? ' ↑' : ' ↓';
renderTable();
}
function badgeClass(stage) {
return { Discovery:'badge-discovery', Proposal:'badge-proposal',
'In Progress':'badge-progress', Launched:'badge-launched', Lost:'badge-lost' }[stage] || 'badge-discovery';
}
function renderTable() {
const rows = getFiltered();
const tbody = document.getElementById('tbody');
const empty = document.getElementById('empty');
if (!rows.length) {
tbody.innerHTML = '';
empty.style.display = '';
return;
}
empty.style.display = 'none';
tbody.innerHTML = rows.map(l => `
<tr>
<td title="${l.name}">${l.name || '—'}</td>
<td class="col-contact" title="${l.contact}">${l.contact || l.email || '—'}</td>
<td class="col-service" title="${l.service}">${l.service || '—'}</td>
<td><span class="badge ${badgeClass(l.stage)}">${l.stage}</span></td>
<td class="col-date">${l.added || '—'}</td>
<td style="text-align:right;white-space:nowrap;">
<button class="act-btn" title="Notes" onclick="openNotes(${l._row})">📝</button>
<button class="act-btn" title="Edit" onclick="openEdit(${l._row})">✏️</button>
<button class="act-btn" title="Delete" onclick="deleteLead(${l._row})">🗑</button>
</td>
</tr>`).join('');
}
function renderAll() { renderStats(); renderTable(); }
// ── UTILS ─────────────────────────────────────
function uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2); }
function today() { return new Date().toLocaleDateString('en-US',{year:'numeric',month:'short',day:'numeric'}); }
let toastTimer;
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.classList.remove('show'), 3000);
}
// close modals on overlay click
document.querySelectorAll('.overlay').forEach(el => {
el.addEventListener('click', e => { if (e.target === el) el.classList.remove('open'); });
});
</script>
</body>
</html>