580 lines
41 KiB
HTML
580 lines
41 KiB
HTML
{{define "content"}}
|
|
<div class="px-8 pt-4 pb-8">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-white mb-1">Organizations</h1>
|
|
<p class="text-[#b0bec5] text-sm">Company directory — parties eligible to participate in deals.</p>
|
|
</div>
|
|
<button id="newOrgBtn" class="hidden px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">+ New Organization</button>
|
|
</div>
|
|
<div id="orgGrid" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="text-[#b0bec5] text-sm col-span-2">Loading...</div>
|
|
</div>
|
|
<div id="emptyState" class="hidden text-center py-20">
|
|
<h2 class="text-xl font-semibold text-white mb-2">No organizations yet</h2>
|
|
<p class="text-[#b0bec5] text-sm">Add organizations to get started.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- New Org Modal -->
|
|
<div id="newOrgModal" class="hidden fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
|
|
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8 w-full max-w-md">
|
|
<h2 class="text-xl font-semibold text-white mb-6">New Organization</h2>
|
|
<div id="modalError" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
|
<div class="space-y-4">
|
|
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Name <span class="text-red-400">*</span></label>
|
|
<input id="oName" type="text" placeholder="Blackstone Group" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c]"></div>
|
|
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Email Domains <span class="text-red-400">*</span></label>
|
|
<input id="oDomains" type="text" placeholder="blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c]">
|
|
<p class="text-[#8899a6] text-xs mt-1">Comma-separated.</p></div>
|
|
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Website</label>
|
|
<input id="oWebsite" type="text" placeholder="blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c]"></div>
|
|
</div>
|
|
<div class="flex gap-3 mt-6">
|
|
<button onclick="closeModal()" class="flex-1 py-2.5 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm font-medium transition">Cancel</button>
|
|
<button id="createBtn" onclick="createOrg()" class="flex-1 py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Create</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Org Modal (tabbed: Details | Members) -->
|
|
<div id="editOrgModal" class="hidden fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
|
|
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl w-full max-w-2xl flex flex-col" style="max-height:90vh">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between px-6 pt-5 pb-0 shrink-0">
|
|
<div class="flex items-center gap-3">
|
|
<div id="eLogoWrap" class="hidden w-10 h-10 rounded-lg border border-white/[0.08] overflow-hidden bg-white shrink-0 flex items-center justify-center">
|
|
<img id="eLogoImg" src="" class="max-w-full max-h-full object-contain">
|
|
</div>
|
|
<h2 id="editOrgTitle" class="text-lg font-semibold text-white">Edit Organization</h2>
|
|
</div>
|
|
<button onclick="closeEditModal()" class="text-[#b0bec5] hover:text-white text-2xl leading-none">×</button>
|
|
</div>
|
|
<!-- Tabs -->
|
|
<div class="flex gap-0 px-6 mt-4 border-b shrink-0" style="border-color:var(--ds-bd)">
|
|
<button id="tabDetails" onclick="switchEditTab('details')" class="px-4 py-2 text-sm font-medium border-b-2 border-[#c9a84c] text-white transition -mb-px">Details</button>
|
|
<button id="tabMembers" onclick="switchEditTab('members')" class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-[#94a3b8] hover:text-white transition -mb-px">Members <span id="memberCountBadge" class="ml-1 px-1.5 py-0.5 rounded-full text-xs bg-white/[0.08]">0</span></button>
|
|
</div>
|
|
<!-- Details Tab -->
|
|
<div id="editTabDetails" class="overflow-y-auto p-6">
|
|
<input type="hidden" id="eOrgId">
|
|
<input type="hidden" id="eVersion">
|
|
<div id="editModalError" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="col-span-2"><label class="block text-xs text-[#b0bec5] mb-1">Name <span class="text-red-400">*</span></label>
|
|
<input id="eName" type="text" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>
|
|
<div class="col-span-2"><label class="block text-xs text-[#b0bec5] mb-1">Description</label>
|
|
<textarea id="eDesc" rows="2" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c] resize-none"></textarea></div>
|
|
<div><label class="block text-xs text-[#b0bec5] mb-1">Industry</label>
|
|
<input id="eIndustry" type="text" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>
|
|
<div><label class="block text-xs text-[#b0bec5] mb-1">Website</label>
|
|
<input id="eWebsite" type="text" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>
|
|
<div><label class="block text-xs text-[#b0bec5] mb-1">Phone</label>
|
|
<input id="ePhone" type="text" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>
|
|
<div><label class="block text-xs text-[#b0bec5] mb-1">LinkedIn</label>
|
|
<input id="eLinkedIn" type="text" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>
|
|
<div><label class="block text-xs text-[#b0bec5] mb-1">Founded</label>
|
|
<input id="eFounded" type="text" placeholder="e.g. 1985" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>
|
|
<div><label class="block text-xs text-[#b0bec5] mb-1">State</label>
|
|
<input id="eState" type="text" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>
|
|
<div class="col-span-2"><label class="block text-xs text-[#b0bec5] mb-1">Address</label>
|
|
<input id="eAddress" type="text" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>
|
|
<div class="col-span-2"><label class="block text-xs text-[#b0bec5] mb-1">Email Domains</label>
|
|
<input id="eDomains" type="text" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
|
<p class="text-xs text-[#64748b] mt-1">Comma-separated. Used to validate invite emails.</p></div>
|
|
<div class="col-span-2"><label class="block text-xs text-[#b0bec5] mb-1">Logo URL</label>
|
|
<input id="eLogo" type="text" placeholder="https://..." class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]" oninput="previewEditLogo()"></div>
|
|
</div>
|
|
</div>
|
|
<!-- Members Tab -->
|
|
<div id="editTabMembers" class="hidden overflow-y-auto p-6 flex-1">
|
|
<!-- Rescrape panel -->
|
|
<div class="flex items-center justify-between mb-4">
|
|
<span class="text-sm font-medium text-white">Team members</span>
|
|
<button id="rescrapeBtn" onclick="startRescrape()" class="flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-medium transition" style="background:var(--ds-hv);color:var(--ds-tx2)">
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
|
Rescrape website
|
|
</button>
|
|
</div>
|
|
<!-- Rescrape results -->
|
|
<div id="rescrapeResults" class="hidden mb-4 rounded-xl overflow-hidden" style="border:1px solid var(--ds-bd)">
|
|
<div class="px-4 py-3 flex items-center justify-between" style="background:var(--ds-hv)">
|
|
<span id="rescrapeTitle" class="text-sm font-medium text-white"></span>
|
|
<button onclick="document.getElementById('rescrapeResults').classList.add('hidden')" class="text-[#94a3b8] hover:text-white text-lg leading-none">×</button>
|
|
</div>
|
|
<div id="rescrapeContent" class="p-4 space-y-3"></div>
|
|
</div>
|
|
<!-- Member list -->
|
|
<div id="memberList" class="space-y-2 mb-4"></div>
|
|
<div id="noMembers" class="hidden text-sm text-center py-6" style="color:var(--ds-tx3)">No members yet</div>
|
|
<!-- Add manually -->
|
|
<div class="pt-3 border-t" style="border-color:var(--ds-bd)">
|
|
<p class="text-xs mb-2 font-medium" style="color:var(--ds-tx3)">Add manually</p>
|
|
<div class="grid gap-2 mb-2" style="grid-template-columns:1fr 1fr">
|
|
<input type="text" id="newMemberName" placeholder="Name" class="px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c] placeholder-[#64748b]">
|
|
<input type="text" id="newMemberTitle" placeholder="Title" class="px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c] placeholder-[#64748b]">
|
|
<input type="email" id="newMemberEmail" placeholder="Email" class="px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c] placeholder-[#64748b]">
|
|
<input type="tel" id="newMemberPhoneBiz" placeholder="Business phone" class="px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c] placeholder-[#64748b]">
|
|
<input type="tel" id="newMemberPhonePersonal" placeholder="Personal phone" onkeydown="if(event.key==='Enter'){event.preventDefault();addGlobalMember();}" class="px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c] placeholder-[#64748b]">
|
|
<button onclick="addGlobalMember()" class="px-3 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-bold rounded-lg text-sm transition">+ Add person</button>
|
|
</div>
|
|
<div id="addMemberError" class="hidden mt-2 text-xs text-red-400"></div>
|
|
</div>
|
|
</div>
|
|
<!-- Footer -->
|
|
<div class="flex gap-3 px-6 py-4 border-t shrink-0" style="border-color:var(--ds-bd)">
|
|
<button onclick="confirmDeleteOrg()" class="px-4 py-2.5 rounded-lg text-sm font-medium transition" style="background:rgba(239,68,68,0.1);color:#ef4444;border:1px solid rgba(239,68,68,0.2)">Delete org</button>
|
|
<div class="flex-1"></div>
|
|
<button onclick="closeEditModal()" class="px-4 py-2.5 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm font-medium transition">Cancel</button>
|
|
<button id="saveOrgBtn" onclick="saveOrg()" class="px-6 py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Save Changes</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
<div id="deleteOrgModal" class="hidden fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
|
|
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl w-full max-w-md p-6">
|
|
<h2 class="text-lg font-semibold text-white mb-2">Delete Organization</h2>
|
|
<p class="text-[#b0bec5] text-sm mb-4">You are about to permanently delete <strong id="deleteOrgName" class="text-white"></strong> from the company registry.</p>
|
|
<div id="deleteDealsWrap" class="hidden mb-4 p-4 rounded-lg" style="background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.2)">
|
|
<p class="text-red-400 text-sm font-medium mb-2">⚠️ Active in the following deals:</p>
|
|
<ul id="dealsList" class="text-sm text-[#b0bec5] space-y-1 list-disc list-inside"></ul>
|
|
<p class="text-red-400 text-xs mt-2">Deleting removes them from all these deals.</p>
|
|
</div>
|
|
<div id="deleteOrgError" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
|
<div class="flex gap-3">
|
|
<button onclick="closeDeleteModal()" class="flex-1 py-2.5 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm font-medium transition">Cancel</button>
|
|
<button id="confirmDeleteBtn" onclick="executeDeleteOrg()" class="flex-1 py-2.5 rounded-lg text-sm font-semibold transition" style="background:#ef4444;color:white">Delete permanently</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{define "scripts"}}
|
|
<script>
|
|
if (user.is_super_admin || user.role === 'ib_admin') document.getElementById('newOrgBtn').classList.remove('hidden');
|
|
|
|
async function loadOrgs() {
|
|
try {
|
|
const res = await fetchAPI('/api/orgs');
|
|
const orgs = await res.json();
|
|
const grid = document.getElementById('orgGrid');
|
|
if (!orgs || orgs.length === 0) {
|
|
grid.classList.add('hidden');
|
|
document.getElementById('emptyState').classList.remove('hidden');
|
|
return;
|
|
}
|
|
window._orgsMap = {};
|
|
orgs.forEach(o => { window._orgsMap[o.entry_id] = o; });
|
|
|
|
grid.innerHTML = orgs.map(o => {
|
|
const name = o.name || 'Untitled';
|
|
const domains = Array.isArray(o.domains) ? o.domains : [];
|
|
const memberCount = Array.isArray(o.members) ? o.members.length : 0;
|
|
const logo = o.logo || '';
|
|
return '<div onclick="openEditModal(\'' + escHtml(o.entry_id) + '\')" class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5 cursor-pointer hover:border-white/[0.2] transition">'
|
|
+ '<div class="flex items-start gap-3 mb-3">'
|
|
+ (logo ? '<div class="w-10 h-10 rounded-lg border border-white/[0.08] overflow-hidden bg-white shrink-0 flex items-center justify-center"><img src="' + escHtml(logo) + '" class="max-w-full max-h-full object-contain" onerror="this.parentElement.style.display=\'none\'"></div>' : '')
|
|
+ '<h3 class="text-white font-semibold leading-tight truncate flex-1">' + escHtml(name) + '</h3>'
|
|
+ '</div>'
|
|
+ (o.description ? '<p class="text-xs mb-2 line-clamp-2" style="color:var(--ds-tx2)">' + escHtml(o.description) + '</p>' : '')
|
|
+ '<div class="flex items-center justify-between mt-1">'
|
|
+ '<div class="flex gap-1.5 flex-wrap">' + domains.map(d => '<span class="text-xs font-mono text-[#b0bec5] bg-white/[0.05] px-2 py-0.5 rounded">@' + escHtml(d) + '</span>').join('') + '</div>'
|
|
+ (memberCount > 0 ? '<span class="text-xs text-[#94a3b8]">' + memberCount + ' member' + (memberCount !== 1 ? 's' : '') + '</span>' : '')
|
|
+ '</div>'
|
|
+ '</div>';
|
|
}).join('');
|
|
} catch(e) {
|
|
document.getElementById('orgGrid').innerHTML = '<div class="text-red-400 text-sm col-span-2">Failed to load.</div>';
|
|
}
|
|
}
|
|
|
|
// ---- New Org ----
|
|
function openModal() { document.getElementById('newOrgModal').classList.remove('hidden'); document.getElementById('oName').focus(); }
|
|
function closeModal() { document.getElementById('newOrgModal').classList.add('hidden'); document.getElementById('modalError').classList.add('hidden'); }
|
|
document.getElementById('newOrgBtn').onclick = openModal;
|
|
|
|
async function createOrg() {
|
|
const name = document.getElementById('oName').value.trim();
|
|
const domainsRaw = document.getElementById('oDomains').value.trim();
|
|
const website = document.getElementById('oWebsite').value.trim().replace(/^https?:\/\//, '');
|
|
const errEl = document.getElementById('modalError');
|
|
const btn = document.getElementById('createBtn');
|
|
if (!name) { errEl.textContent = 'Name is required'; errEl.classList.remove('hidden'); return; }
|
|
if (!domainsRaw) { errEl.textContent = 'At least one domain is required'; errEl.classList.remove('hidden'); return; }
|
|
const domains = domainsRaw.split(',').map(d => d.trim().replace(/^@/, '')).filter(Boolean);
|
|
btn.disabled = true; btn.textContent = 'Creating...'; errEl.classList.add('hidden');
|
|
try {
|
|
const res = await fetchAPI('/api/orgs', { method: 'POST', body: JSON.stringify({ name, domains, website }) });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Failed');
|
|
closeModal();
|
|
['oName','oDomains','oWebsite'].forEach(id => document.getElementById(id).value = '');
|
|
loadOrgs();
|
|
} catch(e) { errEl.textContent = e.message; errEl.classList.remove('hidden'); btn.disabled = false; btn.textContent = 'Create'; }
|
|
}
|
|
|
|
// ---- Edit Org ----
|
|
let editingOrgId = null;
|
|
let editingMembers = [];
|
|
|
|
function switchEditTab(tab) {
|
|
document.getElementById('editTabDetails').classList.toggle('hidden', tab !== 'details');
|
|
document.getElementById('editTabMembers').classList.toggle('hidden', tab !== 'members');
|
|
document.getElementById('tabDetails').className = 'px-4 py-2 text-sm font-medium border-b-2 -mb-px transition ' + (tab === 'details' ? 'border-[#c9a84c] text-white' : 'border-transparent text-[#94a3b8] hover:text-white');
|
|
document.getElementById('tabMembers').className = 'px-4 py-2 text-sm font-medium border-b-2 -mb-px transition ' + (tab === 'members' ? 'border-[#c9a84c] text-white' : 'border-transparent text-[#94a3b8] hover:text-white');
|
|
}
|
|
|
|
function openEditModal(entryId) {
|
|
const o = (window._orgsMap || {})[entryId];
|
|
if (!o) return;
|
|
editingOrgId = o.entry_id;
|
|
editingMembers = (o.members || []).map(m => Object.assign({}, m));
|
|
window._memberExpanded = {};
|
|
document.getElementById('eOrgId').value = o.entry_id;
|
|
document.getElementById('eVersion').value = o.version || 1;
|
|
document.getElementById('eName').value = o.name || '';
|
|
document.getElementById('eDesc').value = o.description || '';
|
|
document.getElementById('eIndustry').value = o.industry || '';
|
|
document.getElementById('eWebsite').value = o.website || '';
|
|
document.getElementById('ePhone').value = o.phone || '';
|
|
document.getElementById('eLinkedIn').value = o.linkedin || '';
|
|
document.getElementById('eFounded').value = o.founded || '';
|
|
document.getElementById('eAddress').value = o.address || '';
|
|
document.getElementById('eState').value = o.state || '';
|
|
document.getElementById('eDomains').value = Array.isArray(o.domains) ? o.domains.join(', ') : '';
|
|
document.getElementById('eLogo').value = o.logo || '';
|
|
document.getElementById('editModalError').classList.add('hidden');
|
|
document.getElementById('editOrgTitle').textContent = o.name || 'Edit Organization';
|
|
const logo = o.logo || '';
|
|
if (logo) { document.getElementById('eLogoImg').src = logo; document.getElementById('eLogoWrap').classList.remove('hidden'); }
|
|
else { document.getElementById('eLogoWrap').classList.add('hidden'); }
|
|
switchEditTab('details');
|
|
document.getElementById('rescrapeResults').classList.add('hidden');
|
|
renderMemberList();
|
|
document.getElementById('editOrgModal').classList.remove('hidden');
|
|
document.getElementById('eName').focus();
|
|
}
|
|
|
|
function closeEditModal() {
|
|
document.getElementById('editOrgModal').classList.add('hidden');
|
|
editingOrgId = null;
|
|
editingMembers = [];
|
|
}
|
|
|
|
function previewEditLogo() {
|
|
const url = document.getElementById('eLogo').value.trim();
|
|
const img = document.getElementById('eLogoImg');
|
|
const wrap = document.getElementById('eLogoWrap');
|
|
if (url) { img.src = url; wrap.classList.remove('hidden'); img.onerror = () => wrap.classList.add('hidden'); }
|
|
else { wrap.classList.add('hidden'); }
|
|
}
|
|
|
|
async function saveOrg() {
|
|
const name = document.getElementById('eName').value.trim();
|
|
const domainsRaw = document.getElementById('eDomains').value.trim();
|
|
const errEl = document.getElementById('editModalError');
|
|
const btn = document.getElementById('saveOrgBtn');
|
|
if (!name) { errEl.textContent = 'Name is required'; errEl.classList.remove('hidden'); return; }
|
|
const domains = domainsRaw ? domainsRaw.split(',').map(d => d.trim().replace(/^@/, '')).filter(Boolean) : undefined;
|
|
btn.disabled = true; btn.textContent = 'Saving...'; errEl.classList.add('hidden');
|
|
try {
|
|
const version = parseInt(document.getElementById('eVersion').value) || 1;
|
|
// Single write: details + members together via members endpoint first, then details
|
|
// Actually: save details (which preserves existing members in orgData), then members
|
|
// To avoid version race, save members FIRST then details (or combine)
|
|
// Simplest: save members first with current version, details second with version+1
|
|
const mRes = await fetchAPI('/api/orgs/' + editingOrgId + '/members', {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ members: editingMembers, version })
|
|
});
|
|
const mData = await mRes.json();
|
|
if (!mRes.ok) throw new Error(mData.error || 'Failed to save members');
|
|
const newVersion = mData.version || version + 1;
|
|
const res = await fetchAPI('/api/orgs/' + editingOrgId, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({
|
|
name, domains,
|
|
description: document.getElementById('eDesc').value.trim(),
|
|
industry: document.getElementById('eIndustry').value.trim(),
|
|
website: document.getElementById('eWebsite').value.trim(),
|
|
phone: document.getElementById('ePhone').value.trim(),
|
|
linkedin: document.getElementById('eLinkedIn').value.trim(),
|
|
founded: document.getElementById('eFounded').value.trim(),
|
|
address: document.getElementById('eAddress').value.trim(),
|
|
state: document.getElementById('eState').value.trim(),
|
|
logo: document.getElementById('eLogo').value.trim(),
|
|
version: newVersion,
|
|
})
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Failed to save details');
|
|
closeEditModal();
|
|
loadOrgs();
|
|
} catch(e) { errEl.textContent = e.message; errEl.classList.remove('hidden'); btn.disabled = false; btn.textContent = 'Save Changes'; }
|
|
}
|
|
|
|
// ---- Members Tab ----
|
|
function renderMemberList() {
|
|
const list = document.getElementById('memberList');
|
|
const none = document.getElementById('noMembers');
|
|
document.getElementById('memberCountBadge').textContent = editingMembers.length;
|
|
if (editingMembers.length === 0) { list.innerHTML = ''; none.classList.remove('hidden'); return; }
|
|
none.classList.add('hidden');
|
|
list.innerHTML = editingMembers.map((m, i) => {
|
|
const initial = (m.name || m.email || '?')[0].toUpperCase();
|
|
const expanded = window._memberExpanded && window._memberExpanded[i];
|
|
return '<div class="rounded-lg overflow-hidden mb-1" style="border:1px solid var(--ds-bd)">'
|
|
// header row
|
|
+ '<div class="flex items-center gap-3 px-3 py-2.5 cursor-pointer" style="background:var(--ds-hv)" onclick="toggleMemberExpand(' + i + ')">'
|
|
+ '<div class="w-8 h-8 rounded-full shrink-0 flex items-center justify-center text-sm font-semibold" style="background:var(--ds-ac);color:var(--ds-act)">' + escHtml(initial) + '</div>'
|
|
+ '<div class="flex-1 min-w-0">'
|
|
+ '<div class="text-sm font-medium truncate" style="color:var(--ds-tx)">' + escHtml(m.name || m.email || '—') + '</div>'
|
|
+ '<div class="text-xs truncate" style="color:var(--ds-tx3)">'
|
|
+ [m.title, m.email].filter(Boolean).map(escHtml).join(' · ')
|
|
+ (m.phone_business ? ' · ' + escHtml(m.phone_business) : '')
|
|
+ '</div>'
|
|
+ '</div>'
|
|
+ '<svg class="w-3.5 h-3.5 shrink-0 transition-transform ' + (expanded ? 'rotate-180' : '') + '" style="color:var(--ds-tx3)" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>'
|
|
+ '</div>'
|
|
// edit panel (collapsed by default)
|
|
+ '<div class="' + (expanded ? '' : 'hidden') + ' p-3" style="background:var(--ds-bg)">'
|
|
+ '<div class="grid gap-2 mb-2" style="grid-template-columns:1fr 1fr">'
|
|
+ '<div><label class="text-xs mb-1 block" style="color:var(--ds-tx3)">Name</label><input type="text" value="' + escHtml(m.name||'') + '" oninput="updateMember(' + i + ',\"name\",this.value)" class="w-full px-2 py-1.5 bg-[#0a1628] border border-white/[0.08] rounded text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>'
|
|
+ '<div><label class="text-xs mb-1 block" style="color:var(--ds-tx3)">Title</label><input type="text" value="' + escHtml(m.title||'') + '" oninput="updateMember(' + i + ',\"title\",this.value)" class="w-full px-2 py-1.5 bg-[#0a1628] border border-white/[0.08] rounded text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>'
|
|
+ '<div><label class="text-xs mb-1 block" style="color:var(--ds-tx3)">Email</label><input type="email" value="' + escHtml(m.email||'') + '" oninput="updateMember(' + i + ',\"email\",this.value)" class="w-full px-2 py-1.5 bg-[#0a1628] border border-white/[0.08] rounded text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>'
|
|
+ '<div><label class="text-xs mb-1 block" style="color:var(--ds-tx3)">Business phone</label><input type="tel" value="' + escHtml(m.phone_business||m.phone||'') + '" oninput="updateMember(' + i + ',\"phone_business\",this.value)" class="w-full px-2 py-1.5 bg-[#0a1628] border border-white/[0.08] rounded text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>'
|
|
+ '<div><label class="text-xs mb-1 block" style="color:var(--ds-tx3)">Personal phone</label><input type="tel" value="' + escHtml(m.phone_personal||'') + '" oninput="updateMember(' + i + ',\"phone_personal\",this.value)" class="w-full px-2 py-1.5 bg-[#0a1628] border border-white/[0.08] rounded text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>'
|
|
+ '</div>'
|
|
+ '<button onclick="removeGlobalMember(' + i + ')" class="px-2 py-1 rounded text-xs transition hover:opacity-80" style="background:rgba(239,68,68,0.1);color:#ef4444;border:1px solid rgba(239,68,68,0.2)">Remove person</button>'
|
|
+ '</div>'
|
|
+ '</div>';
|
|
}).join('');
|
|
}
|
|
|
|
function toggleMemberExpand(i) {
|
|
if (!window._memberExpanded) window._memberExpanded = {};
|
|
window._memberExpanded[i] = !window._memberExpanded[i];
|
|
renderMemberList();
|
|
}
|
|
|
|
function updateMember(i, field, value) {
|
|
if (editingMembers[i]) editingMembers[i][field] = value;
|
|
}
|
|
|
|
function addGlobalMember() {
|
|
const name = document.getElementById('newMemberName').value.trim();
|
|
const email = document.getElementById('newMemberEmail').value.trim();
|
|
const title = document.getElementById('newMemberTitle').value.trim();
|
|
const phone_business = document.getElementById('newMemberPhoneBiz').value.trim();
|
|
const phone_personal = document.getElementById('newMemberPhonePersonal').value.trim();
|
|
const errEl = document.getElementById('addMemberError');
|
|
if (!name && !email) {
|
|
errEl.textContent = 'Enter a name or email.';
|
|
errEl.classList.remove('hidden');
|
|
setTimeout(() => errEl.classList.add('hidden'), 2500);
|
|
document.getElementById('newMemberName').focus();
|
|
return;
|
|
}
|
|
errEl.classList.add('hidden');
|
|
editingMembers.push({ name, email, title, phone_business, phone_personal, photo: '', bio: '', linkedin: '' });
|
|
['newMemberName','newMemberEmail','newMemberTitle','newMemberPhoneBiz','newMemberPhonePersonal'].forEach(id => document.getElementById(id).value = '');
|
|
window._memberExpanded = {};
|
|
renderMemberList();
|
|
document.getElementById('newMemberName').focus();
|
|
}
|
|
|
|
function removeGlobalMember(idx) {
|
|
editingMembers.splice(idx, 1);
|
|
renderMemberList();
|
|
}
|
|
|
|
// ---- Rescrape ----
|
|
async function startRescrape() {
|
|
const btn = document.getElementById('rescrapeBtn');
|
|
const resultsEl = document.getElementById('rescrapeResults');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<svg class="animate-spin w-3.5 h-3.5" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg> Scanning...';
|
|
resultsEl.classList.add('hidden');
|
|
try {
|
|
const res = await fetchAPI('/api/orgs/' + editingOrgId + '/rescrape', { method: 'POST' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Scrape failed');
|
|
renderRescrapeResults(data);
|
|
} catch(e) {
|
|
document.getElementById('rescrapeTitle').textContent = 'Scrape failed: ' + e.message;
|
|
document.getElementById('rescrapeContent').innerHTML = '';
|
|
resultsEl.classList.remove('hidden');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg> Rescrape website';
|
|
}
|
|
}
|
|
|
|
function renderRescrapeResults(data) {
|
|
const newPeople = data.new_people || [];
|
|
const notFound = data.not_found || [];
|
|
const truncated = data.truncated;
|
|
const total = data.total_found || 0;
|
|
let title = '';
|
|
if (newPeople.length > 0) title = newPeople.length + ' new person' + (newPeople.length !== 1 ? 's' : '') + ' found on website';
|
|
else title = 'No new people found';
|
|
|
|
document.getElementById('rescrapeTitle').textContent = title;
|
|
let html = '';
|
|
|
|
if (newPeople.length > 0) {
|
|
html += '<div class="mb-3">'
|
|
+ '<p class="text-xs font-medium mb-2" style="color:var(--ds-tx2)">New people found — select to add:</p>'
|
|
+ '<div class="space-y-1.5">';
|
|
html += '<label class="flex items-center gap-3 px-3 py-2 rounded-lg cursor-pointer mb-1" style="background:var(--ds-hv)">'
|
|
+ '<input type="checkbox" id="rescrapeSelectAll" class="accent-[#c9a84c] shrink-0" onchange="toggleAllRescrape(this.checked)">'
|
|
+ '<span class="text-sm font-medium" style="color:var(--ds-tx2)">Select all</span>'
|
|
+ '</label>';
|
|
newPeople.forEach((p, i) => {
|
|
html += '<div class="rounded-lg overflow-hidden" style="background:var(--ds-sf);border:1px solid var(--ds-bd)">'
|
|
// checkbox + name row
|
|
+ '<div class="flex items-center gap-3 px-3 py-2 cursor-pointer" onclick="toggleScrapeExpand(' + i + ')">'
|
|
+ '<input type="checkbox" class="rescrape-cb accent-[#c9a84c] shrink-0" data-idx="' + i + '" onchange="syncRescrapeSelectAll();event.stopPropagation();" onclick="event.stopPropagation()">'
|
|
+ '<div class="flex-1 min-w-0">'
|
|
+ '<div class="text-sm text-white truncate">' + escHtml(p.name || p.email || '—') + '</div>'
|
|
+ '<div class="text-xs truncate" style="color:var(--ds-tx3)">' + [p.title, p.email].filter(Boolean).map(v => escHtml(v)).join(' · ') + '</div>'
|
|
+ '</div>'
|
|
+ '<span class="text-xs shrink-0" style="color:var(--ds-tx3)">Edit ▾</span>'
|
|
+ '</div>'
|
|
// inline edit panel (hidden by default, shown on click)
|
|
+ '<div id="scrape-edit-' + i + '" class="hidden px-3 pb-3 pt-1" style="background:var(--ds-bg)">'
|
|
+ '<div class="grid gap-2" style="grid-template-columns:1fr 1fr">'
|
|
+ '<div><label class="text-xs mb-1 block" style="color:var(--ds-tx3)">Name</label><input type="text" value="' + escHtml(p.name||'') + '" oninput="updateScrapeNew(' + i + ',\"name\",this.value)" class="w-full px-2 py-1.5 bg-[#0a1628] border border-white/[0.08] rounded text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>'
|
|
+ '<div><label class="text-xs mb-1 block" style="color:var(--ds-tx3)">Title</label><input type="text" value="' + escHtml(p.title||'') + '" oninput="updateScrapeNew(' + i + ',\"title\",this.value)" class="w-full px-2 py-1.5 bg-[#0a1628] border border-white/[0.08] rounded text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>'
|
|
+ '<div><label class="text-xs mb-1 block" style="color:var(--ds-tx3)">Email</label><input type="email" value="' + escHtml(p.email||'') + '" oninput="updateScrapeNew(' + i + ',\"email\",this.value)" class="w-full px-2 py-1.5 bg-[#0a1628] border border-white/[0.08] rounded text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>'
|
|
+ '<div><label class="text-xs mb-1 block" style="color:var(--ds-tx3)">Business phone</label><input type="tel" value="' + escHtml(p.phone_business||p.phone||'') + '" oninput="updateScrapeNew(' + i + ',\"phone_business\",this.value)" class="w-full px-2 py-1.5 bg-[#0a1628] border border-white/[0.08] rounded text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>'
|
|
+ '<div><label class="text-xs mb-1 block" style="color:var(--ds-tx3)">Personal phone</label><input type="tel" value="' + escHtml(p.phone_personal||'') + '" oninput="updateScrapeNew(' + i + ',\"phone_personal\",this.value)" class="w-full px-2 py-1.5 bg-[#0a1628] border border-white/[0.08] rounded text-white text-sm focus:outline-none focus:border-[#c9a84c]"></div>'
|
|
+ '</div>'
|
|
+ '</div>'
|
|
+ '</div>';
|
|
});
|
|
html += '</div>'
|
|
+ '<button onclick="saveRescrapeSelection()" class="mt-2 px-3 py-1.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-xs transition">Save my selection</button>'
|
|
+ '</div>';
|
|
}
|
|
if (notFound.length > 0) {
|
|
html += '<div class="pt-3 border-t" style="border-color:var(--ds-bd)">'
|
|
+ '<p class="text-xs font-medium mb-1 text-amber-400">⚠️ ' + notFound.length + ' person' + (notFound.length !== 1 ? 's' : '') + ' in your database not found on the website:</p>'
|
|
+ '<p class="text-xs mb-2" style="color:var(--ds-tx3)">They may have left the organization. Review and remove individually if confirmed.</p>'
|
|
+ '<div class="space-y-1.5">';
|
|
notFound.forEach((p, i) => {
|
|
html += '<div id="nf-row-' + i + '" class="flex items-center gap-3 px-3 py-2 rounded-lg" style="background:rgba(245,158,11,0.05);border:1px solid rgba(245,158,11,0.15)">'
|
|
+ '<div class="flex-1 min-w-0">'
|
|
+ '<div class="text-sm truncate" style="color:var(--ds-tx)">' + escHtml(p.name || p.email) + '</div>'
|
|
+ (p.title ? '<div class="text-xs truncate text-amber-400">' + escHtml(p.title) + '</div>' : '')
|
|
+ '</div>'
|
|
+ '<span class="text-xs text-amber-400 shrink-0 mx-2">Not on site</span>'
|
|
+ '<button onclick="ignoreInactive(' + i + ')" class="shrink-0 px-2 py-1 rounded text-xs transition hover:opacity-80 mr-1" style="background:var(--ds-hv);color:var(--ds-tx2)">Keep</button>'
|
|
+ '<button onclick="confirmRemoveInactive(' + i + ')" class="shrink-0 px-2 py-1 rounded text-xs transition hover:opacity-80" style="background:rgba(239,68,68,0.1);color:#ef4444;border:1px solid rgba(239,68,68,0.2)">Remove</button>'
|
|
+ '</div>';
|
|
});
|
|
html += '</div></div>';
|
|
window._rescrapeNotFound = notFound;
|
|
}
|
|
if (!html) html = '<p class="text-sm text-[#94a3b8]">Everyone in your database was found on the website. No suggestions.</p>';
|
|
document.getElementById('rescrapeContent').innerHTML = html;
|
|
document.getElementById('rescrapeResults').classList.remove('hidden');
|
|
// Store new people for the add button
|
|
window._rescrapeNewPeople = newPeople;
|
|
}
|
|
|
|
function toggleAllRescrape(checked) {
|
|
document.querySelectorAll('.rescrape-cb').forEach(cb => cb.checked = checked);
|
|
}
|
|
function syncRescrapeSelectAll() {
|
|
const all = document.querySelectorAll('.rescrape-cb');
|
|
const checked = document.querySelectorAll('.rescrape-cb:checked');
|
|
const sa = document.getElementById('rescrapeSelectAll');
|
|
if (sa) sa.checked = all.length > 0 && checked.length === all.length;
|
|
}
|
|
function ignoreInactive(idx) {
|
|
const row = document.getElementById('nf-row-' + idx);
|
|
if (row) row.remove();
|
|
if (window._rescrapeNotFound) window._rescrapeNotFound[idx] = null;
|
|
}
|
|
|
|
function confirmRemoveInactive(idx) {
|
|
const people = window._rescrapeNotFound || [];
|
|
const p = people[idx];
|
|
if (!p) return;
|
|
const name = p.name || p.email || 'this person';
|
|
if (!confirm('Remove ' + name + ' from the organization? This cannot be undone.')) return;
|
|
// Remove from editingMembers by matching email or name
|
|
editingMembers = editingMembers.filter(m => {
|
|
if (p.email && m.email && m.email.toLowerCase() === p.email.toLowerCase()) return false;
|
|
if (p.name && m.name && m.name.toLowerCase() === p.name.toLowerCase()) return false;
|
|
return true;
|
|
});
|
|
// Remove from notFound list and re-render
|
|
window._rescrapeNotFound = people.filter((_, i) => i !== idx);
|
|
renderMemberList();
|
|
// Re-render the not-found section
|
|
const nfEl = document.querySelector('#rescrapeContent .border-t');
|
|
if (window._rescrapeNotFound.length === 0 && nfEl) nfEl.remove();
|
|
}
|
|
|
|
function toggleScrapeExpand(i) {
|
|
const el = document.getElementById('scrape-edit-' + i);
|
|
if (el) el.classList.toggle('hidden');
|
|
}
|
|
function updateScrapeNew(i, field, value) {
|
|
if (window._rescrapeNewPeople && window._rescrapeNewPeople[i]) {
|
|
window._rescrapeNewPeople[i][field] = value;
|
|
}
|
|
}
|
|
function addRescrapeSelected() {
|
|
const people = window._rescrapeNewPeople || [];
|
|
const checked = document.querySelectorAll('.rescrape-cb:checked');
|
|
checked.forEach(cb => {
|
|
const p = people[parseInt(cb.dataset.idx)];
|
|
if (p) editingMembers.push({ name: p.name || '', email: p.email || '', title: p.title || '', phone: p.phone || '', photo: p.photo || '', bio: p.bio || '', linkedin: p.linkedin || '' });
|
|
});
|
|
renderMemberList();
|
|
document.getElementById('rescrapeResults').classList.add('hidden');
|
|
}
|
|
|
|
// ---- Delete Org ----
|
|
async function confirmDeleteOrg() {
|
|
if (!editingOrgId) return;
|
|
const o = (window._orgsMap || {})[editingOrgId];
|
|
document.getElementById('deleteOrgName').textContent = o?.name || editingOrgId;
|
|
document.getElementById('deleteOrgError').classList.add('hidden');
|
|
document.getElementById('deleteDealsWrap').classList.add('hidden');
|
|
document.getElementById('dealsList').innerHTML = '';
|
|
document.getElementById('deleteOrgModal').classList.remove('hidden');
|
|
try {
|
|
const res = await fetchAPI('/api/orgs/' + editingOrgId + '/deals');
|
|
const deals = await res.json();
|
|
if (deals && deals.length > 0) {
|
|
document.getElementById('dealsList').innerHTML = deals.map(d => '<li>' + escHtml(d.name) + '</li>').join('');
|
|
document.getElementById('deleteDealsWrap').classList.remove('hidden');
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
function closeDeleteModal() { document.getElementById('deleteOrgModal').classList.add('hidden'); }
|
|
|
|
async function executeDeleteOrg() {
|
|
const btn = document.getElementById('confirmDeleteBtn');
|
|
const errEl = document.getElementById('deleteOrgError');
|
|
btn.disabled = true; btn.textContent = 'Deleting...'; errEl.classList.add('hidden');
|
|
try {
|
|
const res = await fetchAPI('/api/orgs/' + editingOrgId, { method: 'DELETE' });
|
|
if (!res.ok) { const d = await res.json(); throw new Error(d.error || 'Delete failed'); }
|
|
closeDeleteModal(); closeEditModal(); loadOrgs();
|
|
} catch(e) {
|
|
errEl.textContent = e.message; errEl.classList.remove('hidden');
|
|
btn.disabled = false; btn.textContent = 'Delete permanently';
|
|
}
|
|
}
|
|
|
|
loadOrgs();
|
|
</script>
|
|
{{end}}
|