feat: org edit modal — click card to edit name/domains/role/website/desc; PUT /api/orgs/:id
This commit is contained in:
parent
bbce4cf36a
commit
b402d64cb9
|
|
@ -2850,3 +2850,5 @@ func containsAny(s string, subs ...string) bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UpdateOrg handles PUT /api/orgs/{orgId} — update org name, domains, role, website
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ func NewRouter(db *lib.DB, cfg *lib.Config, store lib.ObjectStore, websiteFS fs.
|
|||
// Organizations (platform level)
|
||||
r.Get("/orgs", h.ListOrgs)
|
||||
r.Post("/orgs", h.CreateOrg)
|
||||
r.Put("/orgs/{orgID}", h.UpdateOrg)
|
||||
r.Get("/orgs/{orgID}", h.GetOrg)
|
||||
r.Patch("/orgs/{orgID}", h.UpdateOrg)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{{define "content"}}
|
||||
<div class="p-8 max-w-5xl">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="px-8 pt-4 pb-8 max-w-5xl">
|
||||
<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-[#94a3b8] text-sm">Company directory — parties eligible to participate in deals.</p>
|
||||
|
|
@ -11,7 +11,6 @@
|
|||
<div class="text-[#94a3b8] text-sm col-span-2">Loading...</div>
|
||||
</div>
|
||||
<div id="emptyState" class="hidden text-center py-20">
|
||||
<div class="text-5xl mb-4">🏢</div>
|
||||
<h2 class="text-xl font-semibold text-white mb-2">No organizations yet</h2>
|
||||
<p class="text-[#94a3b8] text-sm">Add buyer, seller, IB, and advisor organizations.</p>
|
||||
</div>
|
||||
|
|
@ -23,17 +22,17 @@
|
|||
<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-[#94a3b8] mb-1.5">Organization Name</label>
|
||||
<input id="oName" type="text" placeholder="James LLC" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Allowed Email Domains <span class="text-red-400">*</span></label>
|
||||
<input id="oDomains" type="text" placeholder="jamesllc.com, kaseya.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]">
|
||||
<p class="text-[#475569] text-xs mt-1">Comma-separated. Only emails from these domains can be invited for this org.</p></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Default Role</label>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Name</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-[#475569] focus:outline-none focus:border-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] 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-[#475569] focus:outline-none focus:border-[#c9a84c]">
|
||||
<p class="text-[#475569] text-xs mt-1">Comma-separated. Used to validate invite emails.</p></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Role</label>
|
||||
<select id="oRole" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white focus:outline-none focus:border-[#c9a84c]">
|
||||
<option value="seller">Seller</option><option value="buyer">Buyer</option><option value="ib">IB Advisor</option><option value="advisor">Advisor</option>
|
||||
</select></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Website</label>
|
||||
<input id="oWebsite" type="url" placeholder="https://jamesllc.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] focus:ring-1 focus:ring-[#c9a84c]"></div>
|
||||
<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-[#475569] 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>
|
||||
|
|
@ -41,38 +40,80 @@
|
|||
</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 p-8 w-full max-w-md">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-semibold text-white">Edit Organization</h2>
|
||||
<button onclick="closeEditModal()" class="text-[#94a3b8] hover:text-white text-2xl leading-none">×</button>
|
||||
</div>
|
||||
<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="space-y-4">
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Name</label>
|
||||
<input id="eName" type="text" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white focus:outline-none focus:border-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Email Domains</label>
|
||||
<input id="eDomains" type="text" placeholder="blackstone.com, pe.blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c]">
|
||||
<p class="text-[#475569] text-xs mt-1">Comma-separated.</p></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Role</label>
|
||||
<select id="eRole" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white focus:outline-none focus:border-[#c9a84c]">
|
||||
<option value="seller">Seller</option><option value="buyer">Buyer</option><option value="ib">IB Advisor</option><option value="advisor">Advisor</option>
|
||||
</select></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Website</label>
|
||||
<input id="eWebsite" 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-[#475569] focus:outline-none focus:border-[#c9a84c]"></div>
|
||||
<div><label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Description</label>
|
||||
<textarea id="eDesc" rows="2" placeholder="Optional notes..." class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] resize-none"></textarea></div>
|
||||
</div>
|
||||
<div class="flex gap-3 mt-6">
|
||||
<button onclick="closeEditModal()" 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="saveOrgBtn" onclick="saveOrg()" class="flex-1 py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "scripts"}}
|
||||
<script>
|
||||
if (user.is_super_admin) { document.getElementById('newOrgBtn').classList.remove('hidden'); }
|
||||
if (user.is_super_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' };
|
||||
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; }
|
||||
if (!orgs || orgs.length === 0) {
|
||||
grid.classList.add('hidden');
|
||||
document.getElementById('emptyState').classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = orgs.map(o => {
|
||||
// API returns flat fields: o.name, o.role, o.domains, o.website
|
||||
const name = o.name || o.summary || 'Untitled';
|
||||
const name = o.name || 'Untitled';
|
||||
const role = o.role || '';
|
||||
const rc = roleColors[role] || 'bg-gray-500/20 text-gray-300';
|
||||
const domains = Array.isArray(o.domains) ? o.domains : (o.domains ? [o.domains] : []);
|
||||
return `<div class="card bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-6 transition">
|
||||
return `<div onclick="openEditModal(${JSON.stringify(JSON.stringify(o))})"
|
||||
class="card bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-6 cursor-pointer hover:border-white/[0.18] transition">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<h3 class="text-white font-semibold text-lg leading-tight">${escHtml(name)}</h3>
|
||||
<span class="ml-2 shrink-0 px-2 py-0.5 rounded-full text-xs font-medium capitalize ${rc}">${role || '?'}</span>
|
||||
<h3 class="text-white font-semibold leading-tight">${escHtml(name)}</h3>
|
||||
<span class="ml-2 shrink-0 px-2 py-0.5 rounded-full text-xs font-medium capitalize ${rc}">${role || '—'}</span>
|
||||
</div>
|
||||
<div class="flex gap-1.5 flex-wrap mb-3">${domains.map(dm => `<span class="text-xs font-mono text-[#94a3b8] bg-white/[0.05] px-2 py-0.5 rounded">@${escHtml(dm)}</span>`).join('')}</div>
|
||||
${o.website ? `<a href="${escHtml(o.website)}" target="_blank" class="text-xs text-[#c9a84c] hover:underline">${escHtml(o.website)}</a>` : ''}
|
||||
<div class="flex gap-1.5 flex-wrap">${domains.map(dm => `<span class="text-xs font-mono text-[#94a3b8] bg-white/[0.05] px-2 py-0.5 rounded">@${escHtml(dm)}</span>`).join('')}</div>
|
||||
${o.website ? `<div class="mt-2 text-xs text-[#94a3b8]">${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>'; }
|
||||
} 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;
|
||||
|
|
@ -81,26 +122,67 @@
|
|||
const name = document.getElementById('oName').value.trim();
|
||||
const domainsRaw = document.getElementById('oDomains').value.trim();
|
||||
const role = document.getElementById('oRole').value;
|
||||
const website = document.getElementById('oWebsite').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 = 'Organization name is required'; errEl.classList.remove('hidden'); return; }
|
||||
if (!domainsRaw) { errEl.textContent = 'At least one email domain is required'; errEl.classList.remove('hidden'); return; }
|
||||
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);
|
||||
if (!domains.length) { errEl.textContent = 'Invalid domain format'; errEl.classList.remove('hidden'); return; }
|
||||
btn.disabled = true; btn.textContent = 'Creating...'; errEl.classList.add('hidden');
|
||||
try {
|
||||
const res = await fetchAPI('/api/orgs', { method: 'POST', body: JSON.stringify({ name, domains, role, website }) });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Failed to create organization');
|
||||
if (!res.ok) throw new Error(data.error || 'Failed');
|
||||
closeModal();
|
||||
document.getElementById('oName').value = '';
|
||||
document.getElementById('oDomains').value = '';
|
||||
document.getElementById('oWebsite').value = '';
|
||||
['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(oJson) {
|
||||
const o = JSON.parse(oJson);
|
||||
editingOrgId = o.entry_id;
|
||||
document.getElementById('eName').value = o.name || '';
|
||||
document.getElementById('eRole').value = o.role || 'seller';
|
||||
document.getElementById('eDomains').value = Array.isArray(o.domains) ? o.domains.join(', ') : (o.domains || '');
|
||||
document.getElementById('eWebsite').value = o.website || '';
|
||||
document.getElementById('eDesc').value = o.description || '';
|
||||
document.getElementById('editModalError').classList.add('hidden');
|
||||
document.getElementById('editOrgModal').classList.remove('hidden');
|
||||
document.getElementById('eName').focus();
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
document.getElementById('editOrgModal').classList.add('hidden');
|
||||
editingOrgId = null;
|
||||
}
|
||||
|
||||
async function saveOrg() {
|
||||
const name = document.getElementById('eName').value.trim();
|
||||
const domainsRaw = document.getElementById('eDomains').value.trim();
|
||||
const role = document.getElementById('eRole').value;
|
||||
const website = document.getElementById('eWebsite').value.trim().replace(/^https?:\/\//, '');
|
||||
const description = document.getElementById('eDesc').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.split(',').map(d => d.trim().replace(/^@/, '')).filter(Boolean);
|
||||
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, role, website, description })
|
||||
});
|
||||
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'; }
|
||||
}
|
||||
|
||||
loadOrgs();
|
||||
</script>
|
||||
{{end}}
|
||||
|
|
|
|||
Loading…
Reference in New Issue