dealspace/portal/templates/app/orgs.html

334 lines
20 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 &mdash; 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 buyer, seller, IB, and advisor organizations.</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. Used to validate invite emails.</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 -->
<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 max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between px-6 pt-5 pb-4 border-b border-white/[0.08]">
<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 flex items-center justify-center shrink-0">
<img id="eLogoImg" src="" class="max-w-full max-h-full object-contain">
</div>
<h2 class="text-lg font-semibold text-white">Edit Organization</h2>
</div>
<button onclick="closeEditModal()" class="text-[#b0bec5] hover:text-white text-2xl leading-none">&times;</button>
</div>
<div class="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="3" 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 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>
<label class="block text-xs text-[#b0bec5] mb-1">City</label>
<input id="eCity" 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">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">Email Domains</label>
<input id="eDomains" type="text" placeholder="blackstone.com, pe.blackstone.com" 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-[#8899a6] text-xs 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>
<div class="flex gap-3 px-6 pb-6">
<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">⚠️ This org is currently 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 will also remove 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');
const roleColors = {
seller: 'bg-blue-500/20 text-blue-300',
buyer: 'bg-green-500/20 text-green-300',
ib: 'bg-[#c9a84c]/20 text-[#c9a84c]',
advisor: 'bg-purple-500/20 text-purple-300'
};
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 : (o.domains ? [o.domains] : []);
const logo = o.logo || '';
return `<div onclick="openEditModal('${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">${escHtml(name)}</h3>
</div>
${o.description ? `<p class="text-xs text-[#94a3b8] mb-2 line-clamp-2">${escHtml(o.description)}</p>` : ''}
<div class="flex gap-1.5 flex-wrap">${domains.map(dm => `<span class="text-xs font-mono text-[#b0bec5] bg-white/[0.05] px-2 py-0.5 rounded">@${escHtml(dm)}</span>`).join('')}</div>
${o.website ? `<div class="mt-2 text-xs text-[#64748b]">${escHtml(o.website)}</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;
function openEditModal(entryId) {
const o = (window._orgsMap || {})[entryId];
if (!o) return;
editingOrgId = o.entry_id;
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('eCity').value = o.city || '';
document.getElementById('eState').value = o.state || '';
document.getElementById('eDomains').value = Array.isArray(o.domains) ? o.domains.join(', ') : (o.domains || '');
document.getElementById('eLogo').value = o.logo || '';
document.getElementById('editModalError').classList.add('hidden');
const logo = o.logo || '';
if (logo) {
document.getElementById('eLogoImg').src = logo;
document.getElementById('eLogoWrap').classList.remove('hidden');
} else {
document.getElementById('eLogoWrap').classList.add('hidden');
}
document.getElementById('editOrgModal').classList.remove('hidden');
document.getElementById('eName').focus();
}
function closeEditModal() {
document.getElementById('editOrgModal').classList.add('hidden');
editingOrgId = null;
}
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 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(),
city: document.getElementById('eCity').value.trim(),
state: document.getElementById('eState').value.trim(),
logo: document.getElementById('eLogo').value.trim(),
version: parseInt(document.getElementById('eVersion').value) || 1,
})
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to save');
closeEditModal();
loadOrgs();
} catch(e) { errEl.textContent = e.message; errEl.classList.remove('hidden'); btn.disabled = false; btn.textContent = 'Save Changes'; }
}
// ---- 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');
// Fetch deals this org is in
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) { /* non-fatal */ }
}
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}}