feat: member inline edit (name/title/email/biz+personal phone); expand/collapse rows; add form includes phone fields

This commit is contained in:
James 2026-03-20 00:41:03 -04:00
parent 5291edfa94
commit ce80aeeb4a
2 changed files with 52 additions and 19 deletions

View File

@ -106,13 +106,15 @@ type DealOrgPerms struct {
// DealOrgMember is a person associated with a deal org.
type DealOrgMember struct {
Name string `json:"name"`
Email string `json:"email"`
Title string `json:"title,omitempty"`
Phone string `json:"phone,omitempty"`
Photo string `json:"photo,omitempty"`
Bio string `json:"bio,omitempty"`
LinkedIn string `json:"linkedin,omitempty"`
Name string `json:"name"`
Email string `json:"email"`
Title string `json:"title,omitempty"`
Phone string `json:"phone,omitempty"` // legacy / kept for compat
PhoneBusiness string `json:"phone_business,omitempty"`
PhonePersonal string `json:"phone_personal,omitempty"`
Photo string `json:"photo,omitempty"`
Bio string `json:"bio,omitempty"`
LinkedIn string `json:"linkedin,omitempty"`
}
// User represents an account.

View File

@ -109,12 +109,14 @@
<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" style="color:var(--ds-tx3)">Add manually</p>
<div class="grid gap-2" style="grid-template-columns:1fr 1fr 1fr auto">
<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="text" id="newMemberTitle" placeholder="Title" 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">+</button>
<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>
@ -228,6 +230,7 @@
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 || '';
@ -318,22 +321,51 @@
none.classList.add('hidden');
list.innerHTML = editingMembers.map((m, i) => {
const initial = (m.name || m.email || '?')[0].toUpperCase();
return '<div class="flex items-center gap-3 px-3 py-2.5 rounded-lg" style="background:var(--ds-hv)">'
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>'
+ (m.title ? '<div class="text-xs truncate" style="color:var(--ds-tx3)">' + escHtml(m.title) + '</div>' : '')
+ (m.email && m.name ? '<div class="text-xs truncate" style="color:var(--ds-tx3)">' + escHtml(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>'
+ '<button onclick="removeGlobalMember(' + 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">Remove</button>'
+ '</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.';
@ -343,10 +375,9 @@
return;
}
errEl.classList.add('hidden');
editingMembers.push({ name, email, title, phone: '', photo: '', bio: '', linkedin: '' });
document.getElementById('newMemberName').value = '';
document.getElementById('newMemberEmail').value = '';
document.getElementById('newMemberTitle').value = '';
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();
}