<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<meta name="robots" content="noindex,nofollow,noarchive,nosnippet">
<title>Chris Master CRM</title>
<style>
*{box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
body{background:#0f172a;color:#f8fafc;margin:0;padding:12px;font-size:13px}
.top{display:flex;flex-wrap:wrap;gap:8px;justify-content:space-between;align-items:center;background:#1e293b;padding:10px 14px;border-radius:10px;margin-bottom:10px;border:1px solid #334155}
.badge{background:#065f46;color:#6ee7b7;padding:4px 10px;border-radius:99px;font-weight:700;font-size:12px}
.tabs{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
.tab{padding:7px 12px;background:#1e293b;color:#cbd5e1;border:1px solid #334155;border-radius:6px;cursor:pointer;font-weight:600;font-size:12px}
.tab.active{background:#2563eb;color:#fff;border-color:#3b82f6}
.bar{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px;align-items:center}
input,select,textarea{background:#0f172a;color:#f8fafc;border:1px solid #334155;border-radius:6px;padding:7px 10px;font-size:12px}
textarea{width:100%;min-height:54px;resize:vertical;line-height:1.35;font-family:inherit}
textarea:focus,input:focus,select:focus{outline:none;border-color:#3b82f6}
.btn{padding:7px 11px;border-radius:6px;border:none;font-weight:700;font-size:12px;cursor:pointer;color:#fff;display:inline-flex;align-items:center;gap:4px;text-decoration:none;white-space:nowrap}
.b-blue{background:#2563eb}.b-green{background:#059669}.b-purp{background:#7c3aed}.b-gray{background:#334155}
.studio{background:#1e1b4b;border:1px solid #4c1d95;padding:10px;border-radius:8px;margin-bottom:10px;display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between}
.tbl-wrap{overflow-x:auto;background:#1e293b;border-radius:10px;border:1px solid #334155}
table{width:100%;border-collapse:collapse;min-width:880px}
th,td{padding:8px 10px;border-bottom:1px solid #334155;text-align:left;vertical-align:top}
th{background:#0f172a;color:#94a3b8;font-size:11px;text-transform:uppercase;cursor:pointer;user-select:none}
tr:hover{background:#263349}
.drawer{background:#0f172a;padding:10px;border-radius:6px;margin-top:6px;display:none;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:6px;font-size:11px;border:1px solid #334155}
.card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:10px}
.card{background:#1e293b;border:1px solid #334155;border-radius:10px;padding:12px}
#pinModal{position:fixed;inset:0;background:rgba(15,23,42,.96);display:none;align-items:center;justify-content:center;z-index:99}
</style>
</head>
<body>
<div id="pinModal">
<div style="background:#1e293b;padding:24px;border-radius:12px;border:1px solid #334155;width:310px;text-align:center">
<h3 style="margin:0 0 8px">🔒 Chris Master CRM</h3>
<p style="color:#94a3b8;font-size:12px;margin:0 0 14px">Enter your private API passcode to unlock.</p>
<input id="pinIn" type="password" placeholder="Passcode" style="width:100%;margin-bottom:10px;text-align:center;font-size:15px">
<button class="btn b-blue" style="width:100%;justify-content:center;padding:10px" onclick="savePin()">Unlock CRM</button>
</div>
</div>
<div class="top">
<div style="display:flex;align-items:center;gap:10px">
<b style="font-size:16px">📋 Chris Master CRM</b>
<span id="countBadge" class="badge">Loading...</span>
</div>
<div style="display:flex;gap:6px;align-items:center">
<button class="btn b-gray" onclick="toggleStudio()">🖼️ MMS Rotator (<span id="imgCnt">0</span>/3)</button>
<button class="btn b-gray" id="viewBtn" onclick="toggleView()">📱 Card View</button>
<button class="btn b-blue" onclick="loadData()">🔄 Sync</button>
<button class="btn b-gray" onclick="lockPin()" title="Lock">🔒</button>
</div>
</div>
<div id="studioBox" class="studio" style="display:none">
<div>
<b>🎨 3-Slot Anti-Spam Graphic Rotator</b>
<div style="color:#c4b5fd;font-size:11px">Upload 1–3 holiday/promo images. Clicking 🖼️ on any customer row copies the next rotated graphic with a unique pixel signature.</div>
</div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<input type="file" id="imgUp" accept="image/*" multiple onchange="loadFiles(this.files)">
<div id="thumbs" style="display:flex;gap:6px"></div>
</div>
</div>
<div class="tabs" id="tabBar"></div>
<div class="bar">
<input id="qIn" placeholder="🔍 Search name, phone, vehicle, VIN, lender, notes..." style="flex:1;min-width:210px" oninput="page=1;render()">
<select id="globalTpl" onchange="applyGlobalTpl(this.value)">
<option value="auto">⚡ Smart Auto-Match (Per Customer)</option>
<option value="check">👋 6-Month Check-In</option>
<option value="bday">🎂 Birthday Greeting</option>
<option value="apr">📉 High-APR Refi / Payment Drop</option>
<option value="upgrade">🚗 30m+ Equity / Trade Upgrade</option>
<option value="anniv">🎉 Purchase Anniversary</option>
<option value="holiday">🦃 Holiday / Promo Greeting</option>
<option value="ref">🤝 VIP Referral Check-In</option>
</select>
<select id="pageSize" onchange="page=1;render()">
<option value="25">25 / page</option>
<option value="50" selected>50 / page</option>
<option value="100">100 / page</option>
<option value="9999">All</option>
</select>
</div>
<div id="mainOut"></div>
<div id="pager" style="display:flex;justify-content:center;gap:8px;margin-top:12px"></div>
<script>
const API_URL = 'https://script.google.com/macros/s/AKfycbxXR1Zzt7wZ2AO7DCU8_185mrZ8rsRSu_AGnbuEFHlGtGbWOgKHbC4JEmx3EC-YMZK3kQ/exec';
let HEADERS = [], ROWS = [], tab = 'all', view = window.innerWidth < 768 ? 'card' : 'table', page = 1, sortCol = -1, sortAsc = true;
let imgs = [], rotIdx = 0;
const TABS = [
{id:'all', lbl:'All Records'},
{id:'due', lbl:'⏰ Due / Queue'},
{id:'bday', lbl:'🎂 Birthdays'},
{id:'apr', lbl:'📉 High APR (≥7%)'},
{id:'upgrade', lbl:'🚗 Upgrade (30m+)'},
{id:'anniv', lbl:'🎉 Anniversaries'},
{id:'sold', lbl:'✅ Sold Only'}
];
function parseMD(s) {
if (!s) return {m:0, d:0, y:0, ts:0};
s = String(s).trim();
let m = 0, d = 0, y = 0;
if (/^\d{4}-\d{2}-\d{2}/.test(s)) {
const p = s.split(/[-T ]/); y = +p[0]; m = +p[1]; d = +p[2];
} else if (/^\d{1,2}[\/\-]\d{1,2}/.test(s)) {
const p = s.split(/[\/\-]/); m = +p[0]; d = +p[1]; y = p[2] ? (+p[2] < 100 ? 2000 + (+p[2]) : +p[2]) : 0;
}
const ts = (y > 1900 && m > 0 && d > 0) ? new Date(y, m - 1, d).getTime() : 0;
return {m, d, y, ts};
}
function daysUntilMD(m, d) {
if (!m || !d) return 999;
const now = new Date(), today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
let next = new Date(now.getFullYear(), m - 1, d);
let diff = Math.round((next - today) / 86400000);
if (diff < -1) { next = new Date(now.getFullYear() + 1, m - 1, d); diff = Math.round((next - today) / 86400000); }
return diff;
}
function buildMsg(tpl, r) {
const fn = r.c[1] || 'there', veh = r.c[11] || 'vehicle', rt = parseFloat(String(r.c[26]).replace(/[^\d.]/g,'')) || 0;
const b = parseMD(r.c[9]), bDiff = daysUntilMD(b.m, b.d);
if (tpl === 'bday') {
const when = bDiff === 0 ? 'today' : (bDiff === 1 ? 'for tomorrow' : `coming up on ${b.m}/${b.d}`);
return `Hi ${fn}! 🎂 This is Chris Buryk at AutoNation—wanted to reach out and wish you a very Happy Birthday ${when}! Hope you have an awesome time celebrating!`;
}
if (tpl === 'apr') return `Hi ${fn}, it's Chris Buryk at AutoNation. I was reviewing your ${veh} file${rt ? ' (at '+rt+'% APR)' : ''} and we have strong rate-drop and payment-reduction options right now. Want me to run a quick 2-minute review for you?`;
if (tpl === 'upgrade') return `Hi ${fn}, Chris Buryk at AutoNation here! Used values on your ${veh} are strong right now. If you're open to upgrading into a new warranty with a clean trade-in, let me know and I'll send over numbers!`;
if (tpl === 'anniv') return `Hi ${fn}! 🎉 Happy anniversary on getting your ${veh} with me at AutoNation! Hope it's still treating you great—let me know if you ever need anything in service or sales!`;
if (tpl === 'holiday') return `Hi ${fn}! Wishing you and your family a wonderful holiday season from Chris Buryk at AutoNation! Thank you again for your business—reach out anytime you need anything!`;
if (tpl === 'ref') return `Hi ${fn}, Chris Buryk at AutoNation checking in! Hope your ${veh} is running great. Remember I always pay out referral perks if any friends or family are looking for a vehicle!`;
return `Hi ${fn}, this is Chris Buryk at AutoNation. Just checking in to see how your ${veh} is treating you—let me know if you need anything at all!`;
}
function pickSmartTpl(r) {
const now = new Date(), curM = now.getMonth() + 1;
const b = parseMD(r.c[9]), s = parseMD(r.c[19]);
const bDiff = daysUntilMD(b.m, b.d);
const apr = parseFloat(String(r.c[26]).replace(/[^\d.]/g,'')) || 0;
const mOwn = s.ts > 0 ? Math.floor((now.getTime() - s.ts) / 2629800000) : 0;
if (b.m === curM || (bDiff >= -1 && bDiff <= 7)) return 'bday';
if (apr >= 7) return 'apr';
if (s.m === curM && mOwn >= 11) return 'anniv';
if (mOwn >= 30) return 'upgrade';
return 'check';
}
async function loadData() {
const key = localStorage.getItem('crm_pin') || '';
if (!key) { document.getElementById('pinModal').style.display = 'flex'; return; }
document.getElementById('countBadge').innerText = 'Syncing...';
try {
const res = await fetch(`${API_URL}?key=${encodeURIComponent(key)}`);
const j = await res.json();
if (j.status === 'unauthorized') { localStorage.removeItem('crm_pin'); document.getElementById('pinModal').style.display = 'flex'; return; }
HEADERS = j.headers || [];
ROWS = (j.rows || []).reverse().map(r => {
const smart = pickSmartTpl(r);
return { ...r, tpl: smart, msg: buildMsg(smart, r) };
});
renderTabs(); render();
} catch (e) {
document.getElementById('countBadge').innerText = 'Sync Error';
}
}
function savePin() {
const v = document.getElementById('pinIn').value.trim();
if (!v) return;
localStorage.setItem('crm_pin', v);
document.getElementById('pinModal').style.display = 'none';
loadData();
}
function lockPin() { localStorage.removeItem('crm_pin'); location.reload(); }
function toggleStudio() { const el = document.getElementById('studioBox'); el.style.display = el.style.display === 'none' ? 'flex' : 'none'; }
function toggleView() { view = view === 'table' ? 'card' : 'table'; document.getElementById('viewBtn').innerText = view === 'table' ? '📱 Card View' : '💻 Table View'; render(); }
function renderTabs() {
document.getElementById('tabBar').innerHTML = TABS.map(t => `<button class="tab ${tab===t.id?'active':''}" onclick="setTab('${t.id}')">${t.lbl}</button>`).join('');
}
function setTab(t) { tab = t; page = 1; renderTabs(); render(); }
function applyGlobalTpl(val) {
ROWS.forEach(r => {
r.tpl = val === 'auto' ? pickSmartTpl(r) : val;
r.msg = buildMsg(r.tpl, r);
});
render();
}
function updateRowTpl(rowIdx, tpl) {
const r = ROWS.find(x => x.rowIdx === rowIdx);
if (!r) return;
r.tpl = tpl; r.msg = buildMsg(tpl, r);
const box = document.getElementById(`msg_${rowIdx}`);
if (box) box.value = r.msg;
}
function updateRowMsg(rowIdx, val) {
const r = ROWS.find(x => x.rowIdx === rowIdx);
if (r) r.msg = val;
}
function filterRows() {
const q = document.getElementById('qIn').value.toLowerCase().trim();
const now = new Date(), curM = now.getMonth() + 1, todayStr = now.toISOString().slice(0,10);
let list = ROWS.filter(r => {
if (q && !r.c.join(' ').toLowerCase().includes(q)) return false;
const b = parseMD(r.c[9]), s = parseMD(r.c[19]);
const bDiff = daysUntilMD(b.m, b.d);
const apr = parseFloat(String(r.c[26]).replace(/[^\d.]/g,'')) || 0;
const mOwn = s.ts > 0 ? Math.floor((now.getTime() - s.ts) / 2629800000) : 0;
const due = r.c[32] || '';
if (tab === 'bday') return b.m === curM || (bDiff >= -1 && bDiff <= 14);
if (tab === 'apr') return apr >= 7;
if (tab === 'upgrade') return mOwn >= 30;
if (tab === 'anniv') return s.m === curM && mOwn >= 11;
if (tab === 'sold') return /sold/i.test(r.c[18]) || Boolean(r.c[19]);
if (tab === 'due') return (due && due <= todayStr) || (bDiff >= 0 && bDiff <= 3);
return true;
});
if (sortCol >= 0) {
list.sort((a,b) => {
const va = String(a.c[sortCol]||'').toLowerCase(), vb = String(b.c[sortCol]||'').toLowerCase();
return sortAsc ? va.localeCompare(vb, undefined, {numeric:true}) : vb.localeCompare(va, undefined, {numeric:true});
});
}
return list;
}
function sortBy(c) { if (sortCol === c) sortAsc = !sortAsc; else { sortCol = c; sortAsc = true; } render(); }
function render() {
const filtered = filterRows(), sz = +document.getElementById('pageSize').value;
const totalPages = Math.max(1, Math.ceil(filtered.length / sz));
if (page > totalPages) page = totalPages;
const slice = filtered.slice((page - 1) * sz, page * sz);
document.getElementById('countBadge').innerText = `Showing ${slice.length} of ${filtered.length} (Total: ${ROWS.length})`;
const tplOpts = (cur) => [
['check','👋 Check-In'],['bday','🎂 Birthday'],['apr','📉 High-APR'],['upgrade','🚗 Upgrade'],['anniv','🎉 Anniv'],['holiday','🦃 Holiday'],['ref','🤝 Referral']
].map(([k,l]) => `<option value="${k}" ${cur===k?'selected':''}>${l}</option>`).join('');
if (view === 'table') {
let h = `<div class="tbl-wrap"><table><thead><tr>
<th onclick="sortBy(1)">Customer ↕</th>
<th onclick="sortBy(11)">Vehicle & Sold ↕</th>
<th onclick="sortBy(26)">Deal / APR ↕</th>
<th onclick="sortBy(9)">Event / Bday ↕</th>
<th style="min-width:310px">Editable Message (Click to Edit)</th>
<th>Actions</th>
</tr></thead><tbody>`;
slice.forEach(r => {
const ph = String(r.c[3]||'').replace(/\D/g,'').slice(-10);
const det = HEADERS.map((hd, idx) => r.c[idx] ? `<div><span style="color:#94a3b8">${hd}:</span> <b>${r.c[idx]}</b></div>` : '').join('');
const b = parseMD(r.c[9]), bDiff = daysUntilMD(b.m, b.d);
const bTag = b.m ? `<span style="color:${bDiff>=0&&bDiff<=7?'#fbbf24':'#cbd5e1'}">🎂 ${b.m}/${b.d}${bDiff===0?' (TODAY!)':bDiff===1?' (TMRW!)':''}</span>` : '—';
h += `<tr>
<td>
<b style="font-size:14px;color:#fff;cursor:pointer" onclick="togD(${r.rowIdx})">${r.c[1]} ${r.c[2]} ▾</b>
<div style="color:#94a3b8;font-size:11px">${r.c[3]||'No phone'} • ${r.c[18]||'Lead'}</div>
</td>
<td>
<b>${r.c[11]||'—'}</b> ${r.c[20]?`<span style="color:#38bdf8">(${r.c[20]})</span>`:''}
<div style="color:#94a3b8;font-size:11px">Sold: ${r.c[19]||'—'}</div>
</td>
<td>
<div style="color:#34d399;font-weight:600">${r.c[25]?r.c[25]+'/mo':'—'} ${r.c[26]?'@ '+r.c[26]+'%':''}</div>
<div style="color:#94a3b8;font-size:11px">${r.c[28]||''} ${r.c[23]?'• '+r.c[23]:''}</div>
</td>
<td>${bTag}<div style="color:#94a3b8;font-size:11px">Last: ${r.c[31]||'—'}</div></td>
<td>
<select style="margin-bottom:4px;width:100%" onchange="updateRowTpl(${r.rowIdx},this.value)">${tplOpts(r.tpl)}</select>
<textarea id="msg_${r.rowIdx}" oninput="updateRowMsg(${r.rowIdx},this.value)">${r.msg}</textarea>
</td>
<td>
<div style="display:flex;flex-direction:column;gap:5px">
<div style="display:flex;gap:4px">
<button class="btn b-blue" onclick="sendGV(${r.rowIdx},'${ph}')">📱 GVoice</button>
<button class="btn b-purp" onclick="copyRotImg(${r.rowIdx},this)" title="Copy Rotated Graphic">🖼️</button>
</div>
<div style="display:flex;gap:4px">
<a class="btn b-gray" href="tel:${ph}">📞</a>
<button class="btn b-green" onclick="logContact(${r.rowIdx},this)">✅ Log 6m</button>
</div>
</div>
</td>
</tr>
<tr id="dr_${r.rowIdx}" style="display:none"><td colspan="6"><div class="drawer" style="display:grid">${det}</div></td></tr>`;
});
h += `</tbody></table></div>`;
document.getElementById('mainOut').innerHTML = h;
} else {
let h = `<div class="card-grid">`;
slice.forEach(r => {
const ph = String(r.c[3]||'').replace(/\D/g,'').slice(-10);
const det = HEADERS.map((hd, idx) => r.c[idx] ? `<div><span style="color:#94a3b8">${hd}:</span> <b>${r.c[idx]}</b></div>` : '').join('');
h += `<div class="card">
<div style="display:flex;justify-content:space-between;cursor:pointer" onclick="togD(${r.rowIdx})">
<b style="font-size:15px">${r.c[1]} ${r.c[2]}</b>
<span style="color:#60a5fa;font-size:11px">▼ Details</span>
</div>
<div style="color:#cbd5e1;margin:4px 0">${r.c[11]||'No vehicle'} • Sold: ${r.c[19]||'—'}</div>
<div style="color:#34d399;font-size:12px;margin-bottom:6px">🎂 Bday: ${r.c[9]||'—'} • ${r.c[25]||''} ${r.c[26]?'@ '+r.c[26]+'%':''}</div>
<div id="dr_${r.rowIdx}" class="drawer">${det}</div>
<select style="margin:6px 0 4px;width:100%" onchange="updateRowTpl(${r.rowIdx},this.value)">${tplOpts(r.tpl)}</select>
<textarea id="msg_${r.rowIdx}" oninput="updateRowMsg(${r.rowIdx},this.value)">${r.msg}</textarea>
<div style="display:flex;gap:6px;margin-top:8px">
<a class="btn b-gray" href="tel:${ph}">📞</a>
<button class="btn b-blue" style="flex:1;justify-content:center" onclick="sendGV(${r.rowIdx},'${ph}')">📱 GVoice</button>
<button class="btn b-purp" onclick="copyRotImg(${r.rowIdx},this)">🖼️</button>
<button class="btn b-green" onclick="logContact(${r.rowIdx},this)">✅ 6m</button>
</div>
</div>`;
});
h += `</div>`;
document.getElementById('mainOut').innerHTML = h;
}
let pg = '';
if (totalPages > 1) {
for (let p = 1; p <= totalPages; p++) pg += `<button class="tab ${p===page?'active':''}" onclick="page=${p};render()">${p}</button>`;
}
document.getElementById('pager').innerHTML = pg;
}
function togD(id) {
const el = document.getElementById(`dr_${id}`);
if (!el) return;
el.style.display = (el.style.display === 'none' || !el.style.display) ? (view === 'table' ? 'table-row' : 'grid') : 'none';
}
async function sendGV(rowIdx, ph) {
const box = document.getElementById(`msg_${rowIdx}`);
const txt = box ? box.value : '';
try { await navigator.clipboard.writeText(txt); } catch(e){}
window.open(`https://voice.google.com/u/0/messages?number=${ph}`, '_blank');
}
async function logContact(rowIdx, btn) {
const key = localStorage.getItem('crm_pin') || '';
const box = document.getElementById(`msg_${rowIdx}`);
btn.innerText = '⏳';
await fetch(API_URL, { method:'POST', mode:'no-cors', headers:{'Content-Type':'text/plain'}, body:JSON.stringify({action:'markContacted', key, rowIdx, days:180, note: box ? box.value.slice(0,80) : ''}) });
btn.innerText = '✅ Saved'; btn.style.background = '#047857';
}
function loadFiles(files) {
imgs = []; document.getElementById('thumbs').innerHTML = '';
Array.from(files).slice(0,3).forEach((f, idx) => {
const url = URL.createObjectURL(f), im = new Image();
im.onload = () => {
imgs.push(im);
document.getElementById('imgCnt').innerText = imgs.length;
document.getElementById('thumbs').innerHTML += `<img src="${url}" style="height:36px;border-radius:4px;border:1px solid #a78bfa" title="Slot #${idx+1}">`;
};
im.src = url;
});
}
async function copyRotImg(rowIdx, btn) {
if (!imgs.length) { toggleStudio(); alert('Upload 1–3 graphics in the MMS Rotator bar first!'); return; }
const im = imgs[rotIdx % imgs.length], slot = (rotIdx % imgs.length) + 1; rotIdx++;
const cv = document.createElement('canvas'); cv.width = im.width; cv.height = im.height;
const ctx = cv.getContext('2d'); ctx.drawImage(im, 0, 0);
ctx.fillStyle = `rgba(255,255,255,0.015)`; ctx.fillRect(Math.random()*10, Math.random()*10, 2, 2);
cv.toBlob(async (blob) => {
try {
await navigator.clipboard.write([new ClipboardItem({'image/png': blob})]);
btn.innerText = `🖼️#${slot}✅`;
} catch(e) { alert('Clipboard image copy requires HTTPS on Chrome.'); }
}, 'image/png');
}
loadData();
</script>
</body>
</html>