dealspace/portal/templates/app/project.html

1284 lines
66 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{define "header-left"}}
<div class="flex items-center gap-3">
<a href="/app/projects" class="text-2xl font-bold text-white tracking-tight"><span class="text-[#c9a84c]">Deal</span>space</a>
<span class="text-white/20 text-lg">/</span>
<div id="projectSwitcher" class="relative">
<button id="projectSwitcherBtn" onclick="toggleProjectMenu()" class="flex items-center gap-1.5 text-xl font-semibold text-white transition focus:outline-none">
<span id="projectName">Loading...</span>
<span style="font-size:10px;opacity:.5;margin-top:2px"></span>
</button>
<div id="projectMenu" class="hidden absolute left-0 top-full mt-2 min-w-[220px] rounded-lg border shadow-xl z-50" style="background:var(--ds-sf);border-color:var(--ds-bd)">
</div>
</div>
</div>
{{end}}
{{define "content"}}
<div class="p-8" style="max-width:calc(100vw - 14rem)">
<p id="projectDesc" class="text-[#94a3b8] text-sm mb-4"></p>
<!-- Tabs + actions on one line -->
<div class="flex items-end gap-6 border-b border-white/[0.08] mb-6">
<button class="tab active pb-3 text-base font-medium transition" onclick="switchTab('requests', this)">Requests</button>
<button class="tab pb-3 text-base font-medium text-[#94a3b8] transition" onclick="switchTab('orgs', this)">Parties</button>
<div class="ml-auto flex items-center gap-2 pb-2">
<span id="projectStatus" class="px-2 py-0.5 rounded text-xs font-medium"></span>
<button onclick="openImportModal()" class="px-3 py-1 bg-white/[0.07] hover:bg-white/[0.12] text-white rounded text-xs font-medium transition">Import</button>
<button id="detailToggle" onclick="toggleDetailMode()" class="px-3 py-1 bg-white/[0.07] hover:bg-white/[0.12] text-[#cbd5e1] hover:text-white rounded text-xs font-medium transition">Summary</button>
</div>
</div>
<!-- Requests Tab -->
<div id="tab-requests">
<div id="requestEmpty" class="hidden text-center py-16">
<h3 class="text-lg font-semibold text-white mb-1">No requests yet</h3>
<p class="text-[#94a3b8] text-sm mb-4">Import a diligence checklist to get started.</p>
<button onclick="openImportModal()" class="px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Import Checklist</button>
</div>
<div id="treeWrap" class="overflow-x-auto">
<table id="reqTree">
<thead>
<tr>
<th style="width:28px" title="Drag to reorder"></th>
<th style="width:60px">#</th>
<th style="min-width:280px">Request Item</th>
<th style="width:80px">Priority</th>
<th style="width:90px">Status</th>
<th style="width:120px">Assignee</th>
<th style="width:90px">Due Date</th>
<th style="width:90px">Label</th>
<th style="width:50px">Files</th>
<th style="width:160px">Buyer Comment</th>
<th style="width:160px">Seller Comment</th>
<th style="width:90px">Created</th>
<th style="width:40px"></th>
</tr>
</thead>
<tbody id="treeBody">
<tr><td colspan="13" class="text-[#94a3b8] text-sm py-4">Loading requests...</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Orgs Tab -->
<div id="tab-orgs" class="hidden">
<div class="flex justify-between items-center mb-6">
<p class="text-[#94a3b8] text-sm">Parties participating in this deal and their permissions.</p>
<button onclick="openAddOrgModal()" class="px-3 py-1.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">+ Add Org</button>
</div>
<div id="orgList" class="space-y-3"><div class="text-[#94a3b8] text-sm">Loading...</div></div>
</div>
</div>
<!-- Import Modal -->
<div id="importModal" style="display:none" class="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">Import Diligence Checklist</h2>
<div id="importError" 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">Request List Name</label>
<input type="text" id="importListName" placeholder="e.g. Blackstone DDQ, General Checklist"
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm 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">File (CSV or XLSX)</label>
<input type="file" id="importFile" accept=".csv,.xlsx,.xls" required
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm 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">Import Mode</label>
<select id="importMode" 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="add">Add new request list</option>
<option value="replace">Replace all request lists</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Visible to</label>
<select id="importVisibility" 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="">Everyone (General List)</option>
</select>
</div>
</div>
<div class="flex gap-3 mt-6">
<button onclick="closeImportModal()" 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="importBtn" onclick="submitImport()" class="flex-1 py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Import</button>
</div>
</div>
</div>
<!-- Attach Answer Modal -->
<div id="attachModal" style="display:none" class="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-lg">
<h2 class="text-xl font-semibold text-white mb-2">Attach Answer Document</h2>
<p id="attachReqTitle" class="text-[#94a3b8] text-sm mb-4"></p>
<div class="mb-4">
<input type="text" id="attachSearch" placeholder="Search existing documents..."
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]"
oninput="searchAnswers(this.value)">
</div>
<div id="attachList" class="max-h-64 overflow-y-auto space-y-1 mb-4"></div>
<div class="border-t border-white/[0.08] pt-4">
<label class="block text-sm font-medium text-[#94a3b8] mb-1.5">Or upload new document</label>
<input type="file" id="attachFile" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm">
</div>
<div class="flex gap-3 mt-4">
<button onclick="closeAttachModal()" 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 onclick="uploadAndLink()" class="flex-1 py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Upload &amp; Link</button>
</div>
</div>
</div>
<!-- Add Org Modal -->
<div id="addOrgModal" 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-lg max-h-[90vh] overflow-y-auto">
<div class="p-6 border-b border-white/[0.08] flex items-center justify-between">
<h2 class="text-lg font-semibold text-white">Add Organization</h2>
<button onclick="closeAddOrgModal()" class="text-[#94a3b8] hover:text-white transition text-2xl leading-none">&times;</button>
</div>
<div class="p-6 space-y-5">
<div id="addOrgError" class="hidden p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
<!-- Org Type -->
<div>
<label class="block text-sm font-medium text-[#94a3b8] mb-2">Organization Type</label>
<div class="grid grid-cols-4 gap-2">
<button onclick="setOrgType('buyer',this)" data-type="buyer" class="org-type-btn py-2 rounded-lg text-sm border border-white/[0.08] text-[#94a3b8] hover:border-[#c9a84c] hover:text-[#c9a84c] transition">Buyer</button>
<button onclick="setOrgType('seller',this)" data-type="seller" class="org-type-btn py-2 rounded-lg text-sm border border-white/[0.08] text-[#94a3b8] hover:border-[#c9a84c] hover:text-[#c9a84c] transition">Seller</button>
<button onclick="setOrgType('ib',this)" data-type="ib" class="org-type-btn py-2 rounded-lg text-sm border border-white/[0.08] text-[#94a3b8] hover:border-[#c9a84c] hover:text-[#c9a84c] transition">IB Advisor</button>
<button onclick="setOrgType('advisor',this)" data-type="advisor" class="org-type-btn py-2 rounded-lg text-sm border border-white/[0.08] text-[#94a3b8] hover:border-[#c9a84c] hover:text-[#c9a84c] transition">Advisor</button>
</div>
<input type="hidden" id="addOrgType" value="buyer">
</div>
<!-- Org Name with typeahead -->
<div class="relative">
<label class="block text-sm font-medium text-[#94a3b8] mb-2">Organization Name</label>
<input type="text" id="addOrgName" placeholder="Start typing to search or create..." autocomplete="off"
oninput="orgNameSearch(this.value)" onblur="orgNameBlur()"
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] text-sm">
<div id="orgNameDropdown" class="hidden absolute left-0 right-0 top-full mt-1 bg-[#0d1f3c] border border-white/[0.12] rounded-lg z-10 max-h-48 overflow-y-auto shadow-xl"></div>
<input type="hidden" id="addOrgId" value="">
</div>
<!-- Website / Domain -->
<div>
<label class="block text-sm font-medium text-[#94a3b8] mb-2">Website <span class="text-[#475569] font-normal">(used for invite domain validation)</span></label>
<input type="text" id="addOrgWebsite" placeholder="e.g. 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] text-sm">
</div>
<!-- Members -->
<div>
<label class="block text-sm font-medium text-[#94a3b8] mb-2">Team Members</label>
<div id="memberRows" class="space-y-2"></div>
</div>
<!-- Permissions -->
<div class="border-t border-white/[0.08] pt-4">
<p class="text-sm font-medium text-white mb-3">Permissions</p>
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm text-[#94a3b8]">Download documents</span>
<select id="permDownload" class="px-3 py-1.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-xs focus:outline-none focus:border-[#c9a84c]">
<option value="watermark">Watermarked only</option>
<option value="full">Full (no watermark)</option>
<option value="none">No access</option>
</select>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-[#94a3b8]">Upload documents</span>
<input type="checkbox" id="permUpload" class="w-4 h-4 accent-[#c9a84c]">
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-[#94a3b8]">Add request lists</span>
<input type="checkbox" id="permAddLists" class="w-4 h-4 accent-[#c9a84c]">
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-[#94a3b8]">Edit request lists</span>
<input type="checkbox" id="permEditLists" class="w-4 h-4 accent-[#c9a84c]">
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-[#94a3b8]">Folder access</span>
<select id="permFolderAccess" class="px-3 py-1.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-xs focus:outline-none focus:border-[#c9a84c]">
<option value="assigned">Assigned lists only</option>
<option value="all">All request lists</option>
</select>
</div>
</div>
</div>
</div>
<div class="p-6 border-t border-white/[0.08] flex gap-3">
<button onclick="closeAddOrgModal()" 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="addOrgSubmitBtn" onclick="submitAddOrg()" class="flex-1 py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Add to Deal</button>
</div>
</div>
</div>
<!-- Permissions Modal -->
<div id="permModal" 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">
<div class="p-6 border-b border-white/[0.08] flex items-center justify-between">
<h2 class="text-lg font-semibold text-white" id="permModalTitle">Permissions</h2>
<button onclick="closePermModal()" class="text-[#94a3b8] hover:text-white transition text-2xl leading-none">&times;</button>
</div>
<div class="p-6 space-y-4">
<input type="hidden" id="permModalDealOrgId">
<input type="hidden" id="permModalVersion">
<input type="hidden" id="permModalOrgId">
<input type="hidden" id="permModalRole">
<div class="flex items-center justify-between">
<span class="text-sm text-[#94a3b8]">Download documents</span>
<select id="pmDownload" class="px-3 py-1.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-xs focus:outline-none focus:border-[#c9a84c]">
<option value="watermark">Watermarked only</option>
<option value="full">Full (no watermark)</option>
<option value="none">No access</option>
</select>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-[#94a3b8]">Upload documents</span>
<input type="checkbox" id="pmUpload" class="w-4 h-4 accent-[#c9a84c]">
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-[#94a3b8]">Add request lists</span>
<input type="checkbox" id="pmAddLists" class="w-4 h-4 accent-[#c9a84c]">
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-[#94a3b8]">Edit request lists</span>
<input type="checkbox" id="pmEditLists" class="w-4 h-4 accent-[#c9a84c]">
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-[#94a3b8]">Folder access</span>
<select id="pmFolderAccess" class="px-3 py-1.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-xs focus:outline-none focus:border-[#c9a84c]">
<option value="assigned">Assigned lists only</option>
<option value="all">All request lists</option>
</select>
</div>
</div>
<div class="p-6 border-t border-white/[0.08] flex gap-3">
<button onclick="closePermModal()" 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 onclick="savePermissions()" 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>
const projectID = location.pathname.split('/').pop();
function fmtDate(ms) { if (!ms) return ''; const d = new Date(ms); return d.toLocaleDateString('en-US', {month:'short',day:'numeric'}); }
// Badge helpers
const priStyles = { critical:'background:rgba(239,68,68,.15);color:#f87171', high:'background:rgba(251,146,60,.15);color:#fb923c', medium:'background:rgba(250,204,21,.15);color:#facc15', low:'background:rgba(148,163,184,.15);color:#94a3b8' };
const statStyles = { open:'background:rgba(96,165,250,.15);color:#60a5fa', in_process:'background:rgba(251,191,36,.15);color:#fbbf24', partial:'background:rgba(168,85,247,.15);color:#a855f7', complete:'background:rgba(74,222,128,.15);color:#4ade80' };
const statLabels = { open:'Open', in_process:'In Process', partial:'Partial', complete:'Complete' };
function priorityBadge(p) {
return `<span class="badge" style="${priStyles[p]||priStyles.medium}">${escHtml(p||'medium')}</span>`;
}
function statusBadge(s) {
return `<span class="badge" style="${statStyles[s]||statStyles.open}">${statLabels[s]||escHtml(s||'open')}</span>`;
}
function labelPill(label) {
if (!label) return '';
const colors = [
{bg:'rgba(96,165,250,.15)',fg:'#60a5fa'},{bg:'rgba(244,114,182,.15)',fg:'#f472b6'},
{bg:'rgba(167,139,250,.15)',fg:'#a78bfa'},{bg:'rgba(251,146,60,.15)',fg:'#fb923c'},
{bg:'rgba(74,222,128,.15)',fg:'#4ade80'},{bg:'rgba(250,204,21,.15)',fg:'#facc15'}
];
let hash = 0;
for (let i = 0; i < label.length; i++) hash = ((hash << 5) - hash) + label.charCodeAt(i);
const c = colors[Math.abs(hash) % colors.length];
return `<span style="display:inline-block;padding:1px 8px;border-radius:9999px;font-size:11px;background:${c.bg};color:${c.fg}">${escHtml(label)}</span>`;
}
function fmtDueDate(v) {
if (!v) return '\u2014';
if (typeof v === 'number') return fmtDate(v);
return escHtml(String(v));
}
const statusColors = { active: 'bg-green-500/20 text-green-300', draft: 'bg-gray-500/20 text-gray-300', closed: 'bg-red-500/20 text-red-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' };
// ---- State ----
let treeData = [];
let collapsed = new Set();
let numberMap = {};
let openMenuId = null;
let attachRequestId = null;
let detailMode = false;
// ---- Detail toggle ----
function toggleDetailMode() {
detailMode = !detailMode;
document.getElementById('detailToggle').textContent = detailMode ? 'Full' : 'Summary';
renderTree();
}
// ---- Project loading ----
async function loadProject() {
try {
const res = await fetchAPI('/api/projects/' + projectID);
if (!res.ok) { document.getElementById('projectTitle').textContent = 'Not found'; return; }
const raw = await res.json();
const p = raw.project || raw;
const d = parseData(p.data_text);
const name = d.name || p.summary_text || p.summary || 'Untitled';
document.title = name + ' — Dealspace';
document.getElementById('projectName').textContent = name;
fetchAPI('/api/projects').then(r=>r.json()).then(projects=>{
const menu = document.getElementById('projectMenu');
menu.innerHTML = (projects||[]).map(p=>{
const d = p.data_text ? (()=>{try{return JSON.parse(p.data_text)}catch{return{}}})() : {};
const pname = d.name || p.search_key || p.entry_id;
const active = p.entry_id === projectID;
return `<div onclick="switchProject('${p.entry_id}')" class="px-4 py-2.5 cursor-pointer transition text-sm" style="color:${active?'var(--ds-ac)':'var(--ds-tx)'};background:${active?'rgba(255,255,255,.04)':'transparent'}" onmouseover="this.style.background='rgba(255,255,255,.06)'" onmouseout="this.style.background='${active?'rgba(255,255,255,.04)':'transparent'}'">
${escHtml(pname)}
</div>`;
}).join('');
}).catch(()=>{});
// projectTitle removed — name shown in header breadcrumb only
document.getElementById('projectDesc').textContent = d.description || '';
const status = d.status || 'active';
const sc = statusColors[status] || 'bg-gray-500/20 text-gray-300';
document.getElementById('projectStatus').className = 'px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ' + sc;
document.getElementById('projectStatus').textContent = status;
} catch(e) {}
}
// ---- Tree loading ----
async function loadRequestTree() {
try {
const res = await fetchAPI('/api/projects/' + projectID + '/requests/tree');
treeData = await res.json();
if (!treeData || treeData.length === 0) {
document.getElementById('requestEmpty').classList.remove('hidden');
document.getElementById('treeWrap').classList.add('hidden');
return;
}
document.getElementById('requestEmpty').classList.add('hidden');
document.getElementById('treeWrap').classList.remove('hidden');
buildNumbering();
renderTree();
} catch(e) {
document.getElementById('treeBody').innerHTML = '<tr><td colspan="13" class="text-red-400 text-sm py-4">Failed to load requests.</td></tr>';
}
}
// Build hierarchical numbering (1, 1.1, 1.1.1, etc.)
function buildNumbering() {
numberMap = {};
const childrenOf = {};
for (const item of treeData) {
if (!childrenOf[item.parent_id]) childrenOf[item.parent_id] = [];
childrenOf[item.parent_id].push(item.entry_id);
}
const counters = {};
for (const item of treeData) {
if (!counters[item.parent_id]) counters[item.parent_id] = 0;
counters[item.parent_id]++;
const parentNum = numberMap[item.parent_id];
if (item.type === 'request_list') {
numberMap[item.entry_id] = '' + counters[item.parent_id];
} else {
numberMap[item.entry_id] = (parentNum ? parentNum + '.' : '') + counters[item.parent_id];
}
}
}
// Check if an item or any ancestor is collapsed
function isHidden(item) {
const parentMap = {};
for (const t of treeData) parentMap[t.entry_id] = t.parent_id;
let pid = item.parent_id;
while (pid && pid !== projectID) {
if (collapsed.has(pid)) return true;
pid = parentMap[pid];
}
return false;
}
function renderTree() {
const tbody = document.getElementById('treeBody');
let html = '';
for (const item of treeData) {
if (isHidden(item)) continue;
const d = item.data || {};
const num = numberMap[item.entry_id] || '';
const indent = Math.max(0, item.depth - 1) * 20;
const isCollapsible = item.type === 'request_list' || item.type === 'section';
const isCollapsed = collapsed.has(item.entry_id);
const chevron = isCollapsible
? `<span class="collapse-btn ${isCollapsed?'collapsed':''}" onclick="toggleCollapse('${item.entry_id}')">\u25BC</span>`
: '';
if (item.type === 'request_list') {
const name = d.name || 'Request List';
const childCount = item.children_count || 0;
const visOrgId = d.visibility_org_id || '';
const visBadge = visOrgId
? `<span style="display:inline-block;padding:1px 8px;border-radius:9999px;font-size:10px;background:rgba(251,146,60,.15);color:#fb923c;margin-left:8px">Restricted</span>`
: `<span style="display:inline-block;padding:1px 8px;border-radius:9999px;font-size:10px;background:rgba(74,222,128,.12);color:#4ade80;margin-left:8px">All</span>`;
html += `<tr class="row-list" data-id="${item.entry_id}" data-type="${item.type}" data-parent="${item.parent_id}" data-depth="${item.depth}">
<td></td>
<td colspan="12" style="padding-left:${indent+10}px">
${chevron}
<strong style="color:var(--ds-tx);margin-left:4px;font-size:13px;font-weight:600">${escHtml(name)}</strong>
${visBadge}
<span style="color:var(--ds-tx3);margin-left:8px;font-size:11px">${childCount} items</span>
<button onclick="editVisibility('${item.entry_id}')" style="margin-left:6px;background:none;border:none;cursor:pointer;color:var(--ds-tx3);font-size:11px;padding:1px 4px" title="Change visibility">&#9881;</button>
</td>
</tr>`;
} else if (item.type === 'section') {
const name = d.name || d.title || 'Section';
html += `<tr class="row-section" data-id="${item.entry_id}" data-type="${item.type}" data-parent="${item.parent_id}" data-depth="${item.depth}"
draggable="true" ondragstart="onDragStart(event)" ondragover="onDragOver(event)" ondragleave="onDragLeave(event)" ondrop="onDrop(event)">
<td class="drag-handle" style="color:var(--ds-tx3);font-size:11px;cursor:grab">\u2630</td>
<td class="row-num" style="padding-left:${indent}px">${escHtml(num)}</td>
<td colspan="11" style="padding-left:${indent}px">
${chevron}
<span style="color:var(--ds-tx);font-weight:600;font-size:13px;margin-left:4px">${escHtml(name)}</span>
<span style="color:var(--ds-tx3);margin-left:8px;font-size:11px">${item.children_count||0} requests</span>
</td>
</tr>`;
} else {
// request row
const title = d.title || d.description || 'Untitled';
const buyerComment = d.buyer_comment || '';
const sellerComment = d.seller_comment || '';
const assignee = d.assignee || d.assigned_to || '';
const dueDate = d.due_date || d.completed_date || '';
const label = d.label || '';
const curPri = d.priority || 'medium';
const curStat = d.status || 'open';
const eid = item.entry_id;
// Title with detail mode
let titleHtml;
if (detailMode) {
titleHtml = `<a href="/app/requests/${eid}" style="color:var(--ds-tx);text-decoration:none" class="hover:underline">${escHtml(title)}</a>`;
if (d.description) {
titleHtml += `<div style="color:var(--ds-tx3);font-size:12px;margin-top:2px;line-height:1.4">${escHtml(d.description)}</div>`;
}
} else {
// Show first complete sentence, or full title if short — no arbitrary mid-word cut-off
let displayTitle = title;
if (title.length > 120) {
const sentenceEnd = title.search(/[.!?]\s/);
if (sentenceEnd > 20 && sentenceEnd < 160) {
displayTitle = title.substring(0, sentenceEnd + 1);
} else {
// Break at last word boundary before 120
const cut = title.lastIndexOf(' ', 120);
displayTitle = title.substring(0, cut > 40 ? cut : 120);
}
}
titleHtml = `<a href="/app/requests/${eid}" style="color:var(--ds-tx);text-decoration:none;line-height:1.4" class="hover:underline">${escHtml(displayTitle)}</a>`;
}
// Priority select
const priOpts = ['critical','high','medium','low'].map(o =>
`<option value="${o}" ${o===curPri?'selected':''}>${o}</option>`
).join('');
const prioritySelect = `<select class="badge" style="${priStyles[curPri]||priStyles.medium};border:none;cursor:pointer;font-size:11px;padding:2px 14px 2px 6px;border-radius:9999px;-webkit-appearance:none;appearance:none;outline:none;font-weight:500" onchange="updateField('${eid}','priority',this.value)">${priOpts}</select>`;
// Status select
const statOpts = ['open','in_process','partial','complete'].map(o =>
`<option value="${o}" ${o===curStat?'selected':''}>${statLabels[o]}</option>`
).join('');
const statusSelect = `<select class="badge" style="${statStyles[curStat]||statStyles.open};border:none;cursor:pointer;font-size:11px;padding:2px 14px 2px 6px;border-radius:9999px;-webkit-appearance:none;appearance:none;outline:none;font-weight:500" onchange="updateField('${eid}','status',this.value)">${statOpts}</select>`;
html += `<tr class="row-request" data-id="${eid}" data-type="${item.type}" data-parent="${item.parent_id}" data-depth="${item.depth}"
draggable="true" ondragstart="onDragStart(event)" ondragover="onDragOver(event)" ondragleave="onDragLeave(event)" ondrop="onDrop(event)">
<td class="drag-handle" style="color:var(--ds-tx3);font-size:11px">\u2630</td>
<td class="row-num" style="padding-left:${indent}px">${escHtml(num)}</td>
<td class="req-title-cell" style="white-space:normal;max-width:400px;padding-left:${indent}px">
${titleHtml}
</td>
<td>${prioritySelect}</td>
<td>${statusSelect}</td>
<td style="max-width:120px;overflow:hidden;text-overflow:ellipsis;color:var(--ds-tx2);font-size:12px">${assignee ? escHtml(assignee) : '\u2014'}</td>
<td style="color:var(--ds-tx2);font-size:12px">${dueDate ? fmtDueDate(dueDate) : '\u2014'}</td>
<td>${labelPill(label)}</td>
<td style="text-align:center">
${item.answer_count > 0
? `<span class="badge" style="background:rgba(74,222,128,.15);color:#4ade80;cursor:pointer" onclick="showAttachModal('${eid}','${escHtml(title)}')">${item.answer_count}</span>`
: `<span style="color:var(--ds-tx3);cursor:pointer;font-size:12px" onclick="showAttachModal('${eid}','${escHtml(title)}')">&mdash;</span>`}
</td>
<td style="max-width:140px;overflow:hidden;text-overflow:ellipsis;color:var(--ds-tx2);font-size:12px" title="${escHtml(buyerComment)}">${escHtml(buyerComment)}</td>
<td style="max-width:140px;overflow:hidden;text-overflow:ellipsis;color:var(--ds-tx2);font-size:12px" title="${escHtml(sellerComment)}">${escHtml(sellerComment)}</td>
<td style="color:var(--ds-tx3);font-size:12px">${fmtDate(item.created_at)}</td>
<td style="position:relative">
<button onclick="toggleMenu(event,'${eid}')" style="background:none;border:none;cursor:pointer;color:var(--ds-tx3);font-size:16px;padding:2px 6px">&hellip;</button>
<div id="menu-${eid}" class="actions-menu" style="display:none">
<button onclick="showAttachModal('${eid}','${escHtml(title)}')">Attach Answer</button>
<button onclick="editRequest('${eid}', event)">Edit</button>
<button onclick="deleteEntry('${eid}')">Delete</button>
</div>
</td>
</tr>`;
}
}
if (!html) html = '<tr><td colspan="13" class="text-[#94a3b8] text-sm py-4">No requests found.</td></tr>';
tbody.innerHTML = html;
}
function toggleCollapse(id) {
if (collapsed.has(id)) collapsed.delete(id); else collapsed.add(id);
renderTree();
}
// ---- Inline field update ----
async function updateField(entryId, field, value) {
const item = treeData.find(t => t.entry_id === entryId);
if (!item) return;
const d = { ...(item.data || {}), [field]: value };
item.data = d;
try {
const res = await fetchAPI('/api/projects/' + projectID + '/entries/' + entryId, {
method: 'PUT',
body: JSON.stringify({ data: JSON.stringify(d), version: item.version || 0 })
});
if (res.ok) {
const result = await res.json();
if (result.version) item.version = result.version;
renderTree();
}
} catch(e) {
console.error('Update failed:', e);
loadRequestTree();
}
}
// ---- Inline edit request title ----
function editRequest(id, e) {
if (e) e.stopPropagation();
if (openMenuId) {
const m = document.getElementById('menu-' + openMenuId);
if (m) m.style.display = 'none';
openMenuId = null;
}
const item = treeData.find(t => t.entry_id === id);
if (!item) return;
const d = item.data || {};
const title = d.title || d.description || 'Untitled';
const row = document.querySelector(`tr[data-id="${id}"]`);
if (!row) return;
const titleCell = row.querySelector('.req-title-cell');
if (!titleCell) return;
row.dataset.originalTitleHtml = titleCell.innerHTML;
titleCell.innerHTML = `<div style="display:flex;flex-direction:column;gap:4px;width:100%">
<textarea rows="2" style="width:100%;padding:6px 8px;background:#0a1628;border:1px solid rgba(201,168,76,.4);border-radius:6px;color:white;font-size:13px;outline:none;resize:vertical;line-height:1.4;font-family:inherit" id="edit-title-${id}"></textarea>
<div style="display:flex;gap:4px">
<button onclick="saveEditRequest('${id}')" style="padding:3px 12px;background:#c9a84c;color:#0a1628;border:none;border-radius:4px;font-size:11px;font-weight:600;cursor:pointer">Save</button>
<button onclick="cancelEditRequest('${id}')" style="padding:3px 10px;background:rgba(255,255,255,.08);color:white;border:none;border-radius:4px;font-size:11px;cursor:pointer">Cancel</button>
</div>
</div>`;
const input = document.getElementById('edit-title-' + id);
if (input) { input.value = title; input.focus(); input.setSelectionRange(input.value.length, input.value.length); }
}
function cancelEditRequest(id) {
const row = document.querySelector(`tr[data-id="${id}"]`);
if (!row) return;
const titleCell = row.querySelector('.req-title-cell');
if (titleCell && row.dataset.originalTitleHtml) titleCell.innerHTML = row.dataset.originalTitleHtml;
}
async function saveEditRequest(id) {
const input = document.getElementById('edit-title-' + id);
if (!input) return;
const newTitle = input.value.trim();
if (!newTitle) return;
const item = treeData.find(t => t.entry_id === id);
if (!item) return;
const d = { ...(item.data || {}), title: newTitle };
item.data = d;
try {
const res = await fetchAPI('/api/projects/' + projectID + '/entries/' + id, {
method: 'PUT',
body: JSON.stringify({ data: JSON.stringify(d), version: item.version || 0 })
});
if (res.ok) {
const result = await res.json();
if (result.version) item.version = result.version;
renderTree();
}
} catch(e) {
console.error('Save failed:', e);
cancelEditRequest(id);
}
}
// ---- Actions menu ----
function toggleMenu(e, id) {
e.stopPropagation();
if (openMenuId && openMenuId !== id) {
const prev = document.getElementById('menu-' + openMenuId);
if (prev) prev.style.display = 'none';
}
const menu = document.getElementById('menu-' + id);
if (menu) {
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
openMenuId = menu.style.display === 'block' ? id : null;
}
}
document.addEventListener('click', () => {
if (openMenuId) {
const m = document.getElementById('menu-' + openMenuId);
if (m) m.style.display = 'none';
openMenuId = null;
}
});
async function deleteEntry(entryID) {
if (!confirm('Delete this entry?')) return;
await fetchAPI('/api/projects/' + projectID + '/entries/' + entryID, { method: 'DELETE' });
loadRequestTree();
}
// ---- Drag & Drop ----
let dragEntryId = null;
function onDragStart(e) {
dragEntryId = e.currentTarget.dataset.id;
e.dataTransfer.effectAllowed = 'move';
e.currentTarget.style.opacity = '0.5';
}
function onDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const tr = e.currentTarget;
tr.classList.remove('drop-above', 'drop-below', 'drop-into');
const rect = tr.getBoundingClientRect();
const y = e.clientY - rect.top;
const pct = y / rect.height;
if (tr.dataset.type === 'section' && pct > 0.25 && pct < 0.75) {
tr.classList.add('drop-into');
} else if (pct < 0.5) {
tr.classList.add('drop-above');
} else {
tr.classList.add('drop-below');
}
}
function onDragLeave(e) {
e.currentTarget.classList.remove('drop-above', 'drop-below', 'drop-into');
}
async function onDrop(e) {
e.preventDefault();
const tr = e.currentTarget;
tr.classList.remove('drop-above', 'drop-below', 'drop-into');
document.querySelectorAll('#reqTree tr').forEach(r => r.style.opacity = '');
if (!dragEntryId || dragEntryId === tr.dataset.id) return;
const targetId = tr.dataset.id;
const targetType = tr.dataset.type;
const rect = tr.getBoundingClientRect();
const y = e.clientY - rect.top;
const pct = y / rect.height;
let newParentId, position;
if (targetType === 'section' && pct > 0.25 && pct < 0.75) {
newParentId = targetId;
const children = treeData.filter(t => t.parent_id === targetId);
position = children.length;
} else {
newParentId = tr.dataset.parent;
const siblings = treeData.filter(t => t.parent_id === newParentId);
const targetIdx = siblings.findIndex(s => s.entry_id === targetId);
position = pct < 0.5 ? targetIdx : targetIdx + 1;
}
try {
await fetchAPI('/api/projects/' + projectID + '/entries/' + dragEntryId + '/move', {
method: 'POST',
body: JSON.stringify({ parent_id: newParentId, position })
});
await loadRequestTree();
} catch(e) {
console.error('Move failed:', e);
}
dragEntryId = null;
}
// ---- Attach answer modal ----
function showAttachModal(reqId, reqTitle) {
attachRequestId = reqId;
document.getElementById('attachReqTitle').textContent = reqTitle;
document.getElementById('attachModal').style.display = 'flex';
document.getElementById('attachSearch').value = '';
document.getElementById('attachFile').value = '';
loadAnswersForPicker('');
loadLinkedAnswers(reqId);
}
function closeAttachModal() { document.getElementById('attachModal').style.display = 'none'; attachRequestId = null; }
let searchTimeout;
function searchAnswers(q) {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => loadAnswersForPicker(q), 200);
}
async function loadAnswersForPicker(q) {
const list = document.getElementById('attachList');
try {
const res = await fetchAPI('/api/projects/' + projectID + '/answers?q=' + encodeURIComponent(q));
const answers = await res.json();
if (!answers || answers.length === 0) {
list.innerHTML = '<div class="text-[#94a3b8] text-sm py-2">No documents found.</div>';
return;
}
list.innerHTML = answers.map(a => {
const d = a.data || {};
const name = d.filename || d.name || d.title || 'Document';
const objectID = d.object_id || '';
const previewUrl = objectID ? '/api/projects/' + projectID + '/objects/' + objectID + '/preview?filename=' + encodeURIComponent(name) : '';
return `<div class="flex items-center justify-between px-3 py-2 rounded-lg hover:bg-white/[0.04] transition">
<span class="text-white text-sm truncate flex-1">${escHtml(name)}</span>
<div class="flex items-center gap-1.5 shrink-0 ml-2">
${previewUrl ? `<a href="${previewUrl}" target="_blank" class="px-2 py-1 bg-white/[0.06] text-[#94a3b8] hover:text-white rounded text-xs font-medium transition">Preview</a>` : ''}
<button onclick="linkAnswer('${a.entry_id}')" class="px-3 py-1 bg-[#c9a84c]/20 text-[#c9a84c] rounded text-xs font-medium hover:bg-[#c9a84c]/30 transition">Link</button>
</div>
</div>`;
}).join('');
} catch(e) {
list.innerHTML = '<div class="text-red-400 text-sm py-2">Failed to load.</div>';
}
}
async function loadLinkedAnswers(reqId) {
try {
const res = await fetchAPI('/api/projects/' + projectID + '/requests/' + reqId + '/links');
const links = await res.json();
if (links && links.length > 0) {
const linkedHtml = links.map(l => {
const d = l.data || {};
const name = d.filename || d.name || 'Document';
return `<div class="flex items-center justify-between px-3 py-2 rounded-lg bg-green-500/5 border border-green-500/10">
<span class="text-green-300 text-sm truncate flex-1">${escHtml(name)}</span>
<button onclick="unlinkAnswer('${l.answer_id}')" class="ml-2 px-3 py-1 bg-red-500/20 text-red-300 rounded text-xs font-medium hover:bg-red-500/30 transition">Unlink</button>
</div>`;
}).join('');
const list = document.getElementById('attachList');
list.innerHTML = '<div class="text-[#94a3b8] text-xs font-medium mb-1 mt-1">LINKED</div>' + linkedHtml + '<div class="border-t border-white/[0.08] my-2"></div><div class="text-[#94a3b8] text-xs font-medium mb-1">AVAILABLE</div>' + list.innerHTML;
}
} catch(e) {}
}
async function linkAnswer(answerId) {
if (!attachRequestId) return;
await fetchAPI('/api/projects/' + projectID + '/requests/' + attachRequestId + '/links', {
method: 'POST', body: JSON.stringify({ answer_id: answerId })
});
closeAttachModal();
loadRequestTree();
}
async function unlinkAnswer(answerId) {
if (!attachRequestId) return;
await fetchAPI('/api/projects/' + projectID + '/requests/' + attachRequestId + '/links/' + answerId, { method: 'DELETE' });
showAttachModal(attachRequestId, document.getElementById('attachReqTitle').textContent);
}
async function uploadAndLink() {
const fileInput = document.getElementById('attachFile');
const file = fileInput.files[0];
if (!file || !attachRequestId) return;
const fd = new FormData();
fd.append('file', file);
const uploadRes = await fetch('/api/projects/' + projectID + '/objects', {
method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: fd
});
const uploadData = await uploadRes.json();
if (!uploadRes.ok) { alert('Upload failed'); return; }
const answerData = JSON.stringify({ filename: file.name, object_id: uploadData.object_id, name: file.name });
const entryRes = await fetchAPI('/api/projects/' + projectID + '/entries', {
method: 'POST',
body: JSON.stringify({ project_id: projectID, parent_id: projectID, type: 'answer', depth: 1, data: answerData, stage: 'dataroom' })
});
const entry = await entryRes.json();
if (entry.entry_id) {
await fetchAPI('/api/projects/' + projectID + '/requests/' + attachRequestId + '/links', {
method: 'POST', body: JSON.stringify({ answer_id: entry.entry_id })
});
}
closeAttachModal();
loadRequestTree();
}
// ---- Orgs + Team ----
// ---- Organizations ----
let allGlobalOrgs = [];
async function loadOrgs() {
try {
const res = await fetchAPI('/api/projects/' + projectID + '/orgs');
const orgs = await res.json();
const list = document.getElementById('orgList');
if (!orgs || orgs.length === 0) {
list.innerHTML = '<div class="text-[#94a3b8] text-sm">No organizations added yet. Use "+ Add Org" to add parties to this deal.</div>';
return;
}
list.innerHTML = orgs.map(o => {
const rc = roleColors[o.role] || 'bg-gray-500/20 text-gray-300';
const roleLabel = { buyer: 'Buyer', seller: 'Seller', ib: 'IB Advisor', advisor: 'Advisor' }[o.role] || o.role || '?';
const perms = o.permissions || {};
const members = o.members || [];
const dlBadge = { full: '📥 Full download', watermark: '📥 Watermarked', none: '🚫 No download' }[perms.download] || '📥 Watermarked';
return `<div class="rounded-xl bg-[#0d1f3c] border border-white/[0.08] overflow-hidden">
<div class="flex items-center gap-4 px-5 py-4">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span class="text-white font-semibold">${escHtml(o.org_name || 'Unknown')}</span>
<span class="px-2 py-0.5 rounded-full text-xs font-medium ${rc}">${roleLabel}</span>
</div>
<div class="flex flex-wrap gap-2 text-xs text-[#475569] mt-1">
<span>${dlBadge}</span>
${perms.upload ? '<span>📤 Upload</span>' : ''}
${perms.add_request_lists ? '<span> Add lists</span>' : ''}
${perms.edit_request_lists ? '<span>✏️ Edit lists</span>' : ''}
<span>${perms.folder_access === 'all' ? '📂 All folders' : '📂 Assigned only'}</span>
</div>
</div>
<button onclick="openPermModal('${o.deal_org_id}','${o.org_id}','${o.role}',${o.version || 1},${JSON.stringify(JSON.stringify(o.permissions||{}))})"
class="px-3 py-1.5 bg-white/[0.05] hover:bg-white/[0.08] text-[#94a3b8] hover:text-white rounded-lg text-xs transition shrink-0">⚙ Permissions</button>
</div>
${members.length > 0 ? `<div class="border-t border-white/[0.06] px-5 py-3">
<div class="flex flex-wrap gap-3">
${members.map(m => `<div class="flex items-center gap-2">
<div class="w-6 h-6 rounded-full bg-[#c9a84c]/20 flex items-center justify-center text-[#c9a84c] text-xs font-semibold">${(m.name||m.email||'?')[0].toUpperCase()}</div>
<div>
<div class="text-white text-xs font-medium">${escHtml(m.name||m.email)}</div>
${m.title ? `<div class="text-[#475569] text-xs">${escHtml(m.title)}</div>` : ''}
</div>
</div>`).join('')}
</div>
</div>` : ''}
</div>`;
}).join('');
} catch(e) { console.error(e); }
}
// ---- Add Org Modal ----
let orgSearchTimeout;
let pendingMembers = []; // array of {name, email, title}
function openAddOrgModal() {
document.getElementById('addOrgModal').classList.remove('hidden');
document.getElementById('addOrgError').classList.add('hidden');
document.getElementById('addOrgName').value = '';
document.getElementById('addOrgId').value = '';
document.getElementById('addOrgWebsite').value = '';
pendingMembers = [];
renderMemberBubbles();
setOrgType('buyer', document.querySelector('[data-type="buyer"]'));
loadGlobalOrgs();
}
function switchProject(pid) {
document.getElementById('projectMenu').classList.add('hidden');
if (pid && pid !== projectID) window.location.href = '/app/projects/' + pid;
}
function toggleProjectMenu() {
document.getElementById('projectMenu').classList.toggle('hidden');
}
document.addEventListener('click', e => {
if (!document.getElementById('projectSwitcher')?.contains(e.target))
document.getElementById('projectMenu')?.classList.add('hidden');
});
function closeAddOrgModal() {
document.getElementById('addOrgModal').classList.add('hidden');
document.getElementById('orgNameDropdown').classList.add('hidden');
}
async function loadGlobalOrgs() {
if (allGlobalOrgs.length) return;
try {
const res = await fetchAPI('/api/orgs');
allGlobalOrgs = await res.json() || [];
} catch(e) { allGlobalOrgs = []; }
}
function setOrgType(type, btn) {
document.querySelectorAll('.org-type-btn').forEach(b => {
b.classList.remove('border-[#c9a84c]', 'text-[#c9a84c]', 'bg-[#c9a84c]/10');
b.classList.add('border-white/[0.08]', 'text-[#94a3b8]');
});
if (btn) {
btn.classList.add('border-[#c9a84c]', 'text-[#c9a84c]', 'bg-[#c9a84c]/10');
btn.classList.remove('border-white/[0.08]', 'text-[#94a3b8]');
}
document.getElementById('addOrgType').value = type;
}
function orgNameSearch(q) {
clearTimeout(orgSearchTimeout);
const dd = document.getElementById('orgNameDropdown');
document.getElementById('addOrgId').value = '';
if (!q) { dd.classList.add('hidden'); return; }
orgSearchTimeout = setTimeout(() => {
const matches = allGlobalOrgs.filter(o => {
const d = parseData(o.data_text);
return (d.name || '').toLowerCase().includes(q.toLowerCase());
});
if (!matches.length) {
dd.innerHTML = `<div class="px-4 py-3 text-sm text-[#94a3b8]">No match — <strong class="text-white">${escHtml(q)}</strong> will be created as a new org</div>`;
} else {
dd.innerHTML = matches.map(o => {
const d = parseData(o.data_text);
const name = d.name || 'Unnamed';
return `<div onclick="selectOrg('${o.entry_id}','${name.replace(/'/g,"\'")}','${d.role||''}')"
class="px-4 py-3 text-sm text-white hover:bg-white/[0.06] cursor-pointer flex items-center gap-3">
<span class="font-medium">${escHtml(name)}</span>
<span class="text-xs text-[#475569] capitalize">${d.role || ''}</span>
</div>`;
}).join('');
}
dd.classList.remove('hidden');
}, 200);
}
function orgNameBlur() {
// Hide dropdown shortly after blur (allow click to register first)
setTimeout(() => document.getElementById('orgNameDropdown').classList.add('hidden'), 200);
}
function selectOrg(orgId, orgName, role) {
document.getElementById('addOrgId').value = orgId;
document.getElementById('addOrgName').value = orgName;
if (role) setOrgType(role, document.querySelector(`[data-type="${role}"]`));
document.getElementById('orgNameDropdown').classList.add('hidden');
// Load members of this org for suggestions
loadOrgMemberSuggestions(orgId);
}
let orgMemberSuggestions = [];
async function loadOrgMemberSuggestions(orgId) {
orgMemberSuggestions = [];
if (!orgId) return;
// Get members from existing deal_org entries that reference this org
try {
const res = await fetchAPI('/api/orgs/' + orgId);
const org = await res.json();
if (org && org.members) orgMemberSuggestions = org.members;
} catch(e) {}
}
// ---- Member bubble system ----
function renderMemberBubbles() {
const container = document.getElementById('memberRows');
const bubbles = pendingMembers.map((m, i) =>
`<div class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-[#c9a84c]/15 border border-[#c9a84c]/30 rounded-full text-sm text-white">
<span class="w-5 h-5 rounded-full bg-[#c9a84c]/30 flex items-center justify-center text-xs font-semibold text-[#c9a84c]">${(m.name||'?')[0].toUpperCase()}</span>
<span class="font-medium">${escHtml(m.name)}</span>
${m.title ? `<span class="text-[#94a3b8] text-xs">· ${escHtml(m.title)}</span>` : ''}
<button onclick="removeMember(${i})" class="text-[#94a3b8] hover:text-red-400 transition leading-none ml-1">&times;</button>
</div>`
).join('');
container.innerHTML = `
<div class="flex flex-wrap gap-2 mb-2">${bubbles}</div>
<div class="space-y-2" id="member-input-row">
<div class="relative">
<input type="text" id="new-mname" placeholder="Full name" autocomplete="off"
oninput="memberNameSearch(this.value)"
class="w-full px-3 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] text-sm focus:outline-none focus:border-[#c9a84c]">
<div id="memberNameDropdown" class="hidden absolute left-0 right-0 top-full mt-1 bg-[#0d1f3c] border border-white/[0.12] rounded-lg z-20 max-h-32 overflow-y-auto shadow-xl"></div>
</div>
<div class="grid grid-cols-[1fr_1fr_auto] gap-2">
<input type="email" id="new-memail" placeholder="Email"
class="px-3 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] text-sm focus:outline-none focus:border-[#c9a84c]">
<input type="text" id="new-mtitle" placeholder="Title / Role"
onkeydown="if(event.key==='Enter'){event.preventDefault();commitMember();}"
class="px-3 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] text-sm focus:outline-none focus:border-[#c9a84c]">
<button onclick="commitMember()" class="px-3 py-2.5 rounded-lg bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] text-sm font-bold transition">Add</button>
</div>
</div>`;
}
function memberNameSearch(q) {
const dd = document.getElementById('memberNameDropdown');
if (!q || !orgMemberSuggestions.length) { dd.classList.add('hidden'); return; }
const matches = orgMemberSuggestions.filter(m =>
(m.name || '').toLowerCase().includes(q.toLowerCase()) ||
(m.email || '').toLowerCase().includes(q.toLowerCase())
);
if (!matches.length) { dd.classList.add('hidden'); return; }
dd.innerHTML = matches.map(m =>
`<div onclick="selectMemberSuggestion(${JSON.stringify(JSON.stringify(m))})"
class="px-3 py-2 text-xs text-white hover:bg-white/[0.06] cursor-pointer">
<span class="font-medium">${escHtml(m.name)}</span>
${m.email ? `<span class="text-[#475569] ml-1">${escHtml(m.email)}</span>` : ''}
</div>`
).join('');
dd.classList.remove('hidden');
}
function selectMemberSuggestion(mJson) {
const m = JSON.parse(mJson);
document.getElementById('new-mname').value = m.name || '';
document.getElementById('new-memail').value = m.email || '';
document.getElementById('new-mtitle').value = m.title || '';
document.getElementById('memberNameDropdown').classList.add('hidden');
}
function commitMember() {
const name = (document.getElementById('new-mname')?.value || '').trim();
const email = (document.getElementById('new-memail')?.value || '').trim();
const title = (document.getElementById('new-mtitle')?.value || '').trim();
if (!name && !email) return;
pendingMembers.push({ name: name || email, email, title });
// Add to org suggestions for future use
if (!orgMemberSuggestions.find(m => m.email === email)) {
orgMemberSuggestions.push({ name, email, title });
}
renderMemberBubbles();
setTimeout(() => document.getElementById('new-mname')?.focus(), 50);
}
function removeMember(idx) {
pendingMembers.splice(idx, 1);
renderMemberBubbles();
}
async function submitAddOrg() {
const name = document.getElementById('addOrgName').value.trim();
const existingOrgId = document.getElementById('addOrgId').value;
const role = document.getElementById('addOrgType').value;
const errEl = document.getElementById('addOrgError');
const btn = document.getElementById('addOrgSubmitBtn');
if (!name) { errEl.textContent = 'Organization name is required'; errEl.classList.remove('hidden'); return; }
// Collect members — commit any in-progress input row first
const inProgName = document.getElementById('new-mname')?.value.trim();
const inProgEmail = document.getElementById('new-memail')?.value.trim();
const inProgTitle = document.getElementById('new-mtitle')?.value.trim();
if (inProgName || inProgEmail) pendingMembers.push({ name: inProgName || inProgEmail, email: inProgEmail, title: inProgTitle });
const members = [...pendingMembers];
const perms = {
download: document.getElementById('permDownload').value,
upload: document.getElementById('permUpload').checked,
add_request_lists: document.getElementById('permAddLists').checked,
edit_request_lists: document.getElementById('permEditLists').checked,
folder_access: document.getElementById('permFolderAccess').value,
};
btn.disabled = true; btn.textContent = 'Adding...'; errEl.classList.add('hidden');
try {
let orgId = existingOrgId;
if (!orgId) {
// Use manually entered website/domain first, fall back to member email domains
const websiteInput = (document.getElementById('addOrgWebsite').value || '').trim().replace(/^https?:\/\//, '').replace(/\/.*$/, '').toLowerCase();
const emailDomains = [...new Set(
members.map(m => m.email ? m.email.split('@')[1] : null).filter(Boolean)
)];
const domains = websiteInput ? [websiteInput, ...emailDomains.filter(d => d !== websiteInput)] : emailDomains;
const createRes = await fetchAPI('/api/orgs', { method: 'POST', body: JSON.stringify({ name, domains, role, website: websiteInput ? 'https://' + websiteInput : '' }) });
if (!createRes.ok) { const e = await createRes.json(); throw new Error(e.error || 'Failed to create org'); }
const created = await createRes.json();
orgId = created.entry_id || created.org_id;
}
// Add to deal
const addRes = await fetchAPI('/api/projects/' + projectID + '/orgs', {
method: 'POST', body: JSON.stringify({ org_id: orgId, role, domain_lock: false })
});
if (!addRes.ok) { const e = await addRes.json(); throw new Error(e.error || 'Failed to add org to deal'); }
const added = await addRes.json();
// Update with permissions + members
if (added.deal_org_id) {
await fetchAPI('/api/projects/' + projectID + '/entries/' + added.deal_org_id, {
method: 'PUT',
body: JSON.stringify({
project_id: projectID, parent_id: projectID, type: 'deal_org', depth: 1,
data: JSON.stringify({ org_id: orgId, role, domain_lock: false, permissions: perms, members }),
version: 1
})
});
}
closeAddOrgModal();
loadOrgs();
} catch(e) {
errEl.textContent = e.message;
errEl.classList.remove('hidden');
} finally {
btn.disabled = false; btn.textContent = 'Add to Deal';
}
}
// ---- Permissions Modal ----
function openPermModal(dealOrgId, orgId, role, version, permsJson) {
let perms = {};
try { perms = JSON.parse(permsJson) || {}; } catch(e) {}
document.getElementById('permModalDealOrgId').value = dealOrgId;
document.getElementById('permModalOrgId').value = orgId;
document.getElementById('permModalRole').value = role;
document.getElementById('permModalVersion').value = version;
const roleLabel = { buyer: 'Buyer', seller: 'Seller', ib: 'IB Advisor', advisor: 'Advisor' }[role] || role;
document.getElementById('permModalTitle').textContent = 'Permissions — ' + roleLabel;
document.getElementById('pmDownload').value = perms.download || 'watermark';
document.getElementById('pmUpload').checked = !!perms.upload;
document.getElementById('pmAddLists').checked = !!perms.add_request_lists;
document.getElementById('pmEditLists').checked = !!perms.edit_request_lists;
document.getElementById('pmFolderAccess').value = perms.folder_access || 'assigned';
document.getElementById('permModal').classList.remove('hidden');
}
function closePermModal() { document.getElementById('permModal').classList.add('hidden'); }
async function savePermissions() {
const dealOrgId = document.getElementById('permModalDealOrgId').value;
const orgId = document.getElementById('permModalOrgId').value;
const role = document.getElementById('permModalRole').value;
const version = parseInt(document.getElementById('permModalVersion').value) || 1;
const perms = {
download: document.getElementById('pmDownload').value,
upload: document.getElementById('pmUpload').checked,
add_request_lists: document.getElementById('pmAddLists').checked,
edit_request_lists: document.getElementById('pmEditLists').checked,
folder_access: document.getElementById('pmFolderAccess').value,
};
try {
await fetchAPI('/api/projects/' + projectID + '/entries/' + dealOrgId, {
method: 'PUT',
body: JSON.stringify({
project_id: projectID, parent_id: projectID, type: 'deal_org', depth: 1,
data: JSON.stringify({ org_id: orgId, role, domain_lock: false, permissions: perms }),
version
})
});
closePermModal();
loadOrgs();
} catch(e) { alert('Failed to save permissions'); }
}
function openPermissionsTemplateModal() {
alert('Coming soon: set default permissions templates per org type for all deals.');
}
function switchTab(name, el) {
document.querySelectorAll('.tab').forEach(t => { t.classList.remove('active','text-white'); t.classList.add('text-[#94a3b8]'); });
el.classList.add('active','text-white'); el.classList.remove('text-[#94a3b8]');
document.getElementById('tab-requests').classList.toggle('hidden', name !== 'requests');
document.getElementById('tab-orgs').classList.toggle('hidden', name !== 'orgs');
if (name === 'orgs') loadOrgs();
}
// ---- Import ----
async function openImportModal() {
document.getElementById('importModal').style.display='flex';
document.getElementById('importFile').value='';
document.getElementById('importListName').value='';
document.getElementById('importError').classList.add('hidden');
// Populate visibility org options
try {
const res = await fetchAPI('/api/projects/' + projectID + '/orgs');
const orgs = await res.json();
const sel = document.getElementById('importVisibility');
sel.innerHTML = '<option value="">Everyone (General List)</option>';
if (orgs && orgs.length > 0) {
for (const o of orgs) {
const d = parseData(o.data_text);
const orgName = d.org_name || d.name || 'Unknown';
const orgId = d.org_id || o.entry_id || '';
sel.innerHTML += `<option value="${escHtml(orgId)}">${escHtml(orgName)} only</option>`;
}
}
} catch(e) {}
}
function closeImportModal() { document.getElementById('importModal').style.display='none'; }
async function submitImport() {
const file = document.getElementById('importFile').files[0];
const errEl = document.getElementById('importError');
const btn = document.getElementById('importBtn');
if (!file) { errEl.textContent = 'Please select a file'; errEl.classList.remove('hidden'); return; }
btn.disabled = true; btn.textContent = 'Importing...'; errEl.classList.add('hidden');
const fd = new FormData();
fd.append('file', file);
fd.append('mode', document.getElementById('importMode').value);
fd.append('list_name', document.getElementById('importListName').value);
fd.append('visibility_org_id', document.getElementById('importVisibility').value);
try {
const res = await fetch('/api/projects/' + projectID + '/requests/import', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: fd });
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Import failed');
closeImportModal();
loadRequestTree();
} catch(e) { errEl.textContent = e.message; errEl.classList.remove('hidden'); }
finally { btn.disabled = false; btn.textContent = 'Import'; }
}
// ---- Visibility edit ----
async function editVisibility(entryId) {
const item = treeData.find(t => t.entry_id === entryId);
if (!item) return;
const d = item.data || {};
const currentVis = d.visibility_org_id || '';
// Fetch orgs for the selector
let orgs = [];
try {
const res = await fetchAPI('/api/projects/' + projectID + '/orgs');
orgs = await res.json() || [];
} catch(e) {}
let opts = '<option value="">Everyone (General List)</option>';
for (const o of orgs) {
const od = parseData(o.data_text);
const orgName = od.org_name || od.name || 'Unknown';
const orgId = od.org_id || o.entry_id || '';
opts += `<option value="${escHtml(orgId)}" ${orgId === currentVis ? 'selected' : ''}>${escHtml(orgName)} only</option>`;
}
const row = document.querySelector(`tr[data-id="${entryId}"]`);
if (!row) return;
const td = row.querySelector('td:nth-child(2)');
if (!td) return;
td.dataset.origHtml = td.innerHTML;
td.innerHTML += `<div style="margin-top:4px;display:flex;align-items:center;gap:4px">
<select id="vis-sel-${entryId}" style="padding:4px 8px;background:#0a1628;border:1px solid rgba(255,255,255,.12);border-radius:6px;color:white;font-size:12px">${opts}</select>
<button onclick="saveVisibility('${entryId}')" style="padding:2px 8px;background:#c9a84c;color:#0a1628;border:none;border-radius:4px;font-size:11px;font-weight:600;cursor:pointer">Save</button>
<button onclick="cancelVisibility('${entryId}')" style="padding:2px 8px;background:rgba(255,255,255,.08);color:white;border:none;border-radius:4px;font-size:11px;cursor:pointer">Cancel</button>
</div>`;
}
function cancelVisibility(entryId) {
const row = document.querySelector(`tr[data-id="${entryId}"]`);
if (!row) return;
const td = row.querySelector('td:nth-child(2)');
if (td && td.dataset.origHtml) td.innerHTML = td.dataset.origHtml;
}
async function saveVisibility(entryId) {
const sel = document.getElementById('vis-sel-' + entryId);
if (!sel) return;
const val = sel.value || null;
try {
await fetchAPI('/api/projects/' + projectID + '/entries/' + entryId + '/visibility', {
method: 'PATCH',
body: JSON.stringify({ visibility_org_id: val })
});
loadRequestTree();
} catch(e) {
console.error('Visibility update failed:', e);
}
}
// ---- Init ----
loadProject();
loadRequestTree();
</script>
{{end}}