dealspace/portal/templates/app/project.html

1755 lines
92 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="px-8 pt-4 pb-8" style="max-width:calc(100vw - 14rem)">
<p id="projectDesc" class="text-[#b0bec5] text-sm mb-2"></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-[#b0bec5] 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="openCreateListModal()" class="px-3.5 py-1.5 bg-white/[0.07] hover:bg-white/[0.12] text-white rounded text-sm font-medium transition">+ New List</button>
<button onclick="openImportModal()" class="px-3.5 py-1.5 bg-white/[0.07] hover:bg-white/[0.12] text-white rounded text-sm font-medium transition">Import List</button>
<button id="detailToggle" onclick="toggleDetailMode()" class="px-3.5 py-1.5 bg-white/[0.07] hover:bg-white/[0.12] text-[#cbd5e1] hover:text-white rounded text-sm font-medium transition">Summary View</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-[#b0bec5] 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-[#b0bec5] 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-[#b0bec5] 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-[#b0bec5] text-sm">Loading...</div></div>
</div>
</div>
<!-- Create List Modal -->
<div id="createListModal" style="display:none" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
<div class="bg-[#0d1f3c] rounded-xl p-6 w-full max-w-md border border-white/[0.08]">
<h2 class="text-lg font-semibold text-white mb-4">New Request List</h2>
<div id="createListError" class="hidden mb-3 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
<div class="mb-4">
<label class="block text-sm font-medium text-[#b0bec5] mb-1.5">List name</label>
<input type="text" id="createListName" placeholder="e.g. Legal Due Diligence, Financial Review"
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] text-sm"
onkeydown="if(event.key==='Enter')submitCreateList()">
</div>
<div class="flex gap-3">
<button onclick="closeCreateListModal()" 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="createListBtn" onclick="submitCreateList()" 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>
<!-- 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-[#b0bec5] 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-[#b0bec5] 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-[#b0bec5] 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-[#b0bec5] 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 List</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-[#b0bec5] text-sm mb-2"></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-[#b0bec5] 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 Party Modal (3-step: email → org details → colleagues) -->
<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-2xl max-h-[90vh] overflow-y-auto">
<!-- Step 1: Email lookup -->
<div id="addOrgStep1" class="p-6">
<div class="flex items-center justify-between mb-1">
<h2 class="text-lg font-semibold text-white">Add Party</h2>
<button onclick="closeAddOrgModal()" class="text-[#b0bec5] hover:text-white transition text-2xl leading-none">&times;</button>
</div>
<p class="text-[#b0bec5] text-sm mb-5">Enter an email address — we'll look up the organization automatically.</p>
<div class="flex gap-3">
<input type="email" id="addOrgEmail" placeholder="e.g. john.smith@example.com" autocomplete="off"
class="flex-1 px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c] placeholder-[#8899a6]"
onkeydown="if(event.key==='Enter')scrapeOrg()">
<button onclick="scrapeOrg()" id="scrapeBtn" class="px-5 py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition whitespace-nowrap">Look up</button>
</div>
<div id="scrapeError" class="hidden mt-3 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm flex items-center justify-between gap-3">
<span id="scrapeErrorMsg"></span>
<button onclick="skipScrape()" class="shrink-0 px-3 py-1 bg-white/[0.08] hover:bg-white/[0.12] text-white rounded text-xs font-medium transition">Continue manually</button>
</div>
<div id="scrapeLoading" class="hidden mt-4 flex items-center gap-3 text-[#b0bec5] text-sm">
<svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
Scanning website...
</div>
</div>
<!-- Step 2: Org details (prefilled from scrape) -->
<div id="addOrgStep2" class="hidden p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-white">Organization Details</h2>
<button onclick="closeAddOrgModal()" class="text-[#b0bec5] hover:text-white transition text-2xl leading-none">&times;</button>
</div>
<div class="grid grid-cols-2 gap-4 mb-5">
<div class="col-span-2 flex items-start gap-4">
<div id="orgLogoWrap" class="hidden shrink-0 w-16 h-16 rounded-lg border border-white/[0.08] overflow-hidden bg-white flex items-center justify-center">
<img id="orgLogoImg" src="" class="max-w-full max-h-full object-contain">
</div>
<div class="flex-1">
<label class="block text-xs text-[#b0bec5] mb-1">Name <span class="text-red-400">*</span></label>
<input type="text" id="orgName" 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>
<div class="col-span-2">
<label class="block text-xs text-[#b0bec5] mb-1">Description</label>
<textarea id="orgDesc" rows="4" 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">Role in deal <span class="text-red-400">*</span></label>
<select id="orgRole" 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]">
<option value="">Select...</option>
<option value="seller">Seller</option>
<option value="buyer">Buyer</option>
<option value="ib">Investment Bank</option>
<option value="advisor">Advisor</option>
</select>
</div>
<div>
<label class="block text-xs text-[#b0bec5] mb-1">Industry</label>
<input type="text" id="orgIndustry" 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 type="text" id="orgWebsite" 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 type="text" id="orgPhone" 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 type="text" id="orgAddress" 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 type="text" id="orgCity" 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 type="text" id="orgState" 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 type="text" id="orgFounded" 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 type="text" id="orgLinkedIn" 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>
<div class="flex justify-between">
<button onclick="showAddOrgStep(1)" class="px-4 py-2 text-[#b0bec5] hover:text-white text-sm transition">Back</button>
<button onclick="validateStep2() && showAddOrgStep(3)" class="px-5 py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Next: Select People</button>
</div>
</div>
<!-- Step 3: Colleagues -->
<div id="addOrgStep3" class="hidden p-6">
<div class="flex items-center justify-between mb-1">
<h2 class="text-lg font-semibold text-white">Select Colleagues</h2>
<button onclick="closeAddOrgModal()" class="text-[#b0bec5] hover:text-white transition text-2xl leading-none">&times;</button>
</div>
<p class="text-[#b0bec5] text-sm mb-4">Choose who to add to this deal.</p>
<div class="flex items-center gap-3 mb-4">
<label class="flex items-center gap-2 cursor-pointer text-sm text-[#b0bec5]">
<input type="checkbox" id="selectAllPeople" onchange="toggleAllPeople(this.checked)" class="rounded accent-[#c9a84c]">
Select all
</label>
<span id="selectedCount" class="text-xs text-[#8899a6]">0 selected</span>
</div>
<div id="peopleList" class="space-y-2 mb-4 max-h-[40vh] overflow-y-auto"></div>
<div id="noPeople" class="hidden text-[#b0bec5] text-sm py-4 text-center">No team members found on the website.</div>
<!-- Manual add row -->
<div class="border-t border-white/[0.08] pt-3 mt-3">
<p class="text-xs text-[#b0bec5] mb-2">Add manually</p>
<div class="grid grid-cols-[1fr_1fr_1fr_auto] gap-2">
<input type="text" id="manualName" 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-[#8899a6]">
<input type="email" id="manualEmail" 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-[#8899a6]">
<input type="text" id="manualTitle" placeholder="Title"
onkeydown="if(event.key==='Enter'){event.preventDefault();addManualPerson();}"
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-[#8899a6]">
<button onclick="addManualPerson()" class="px-3 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-bold rounded-lg text-sm transition">+</button>
</div>
</div>
<div class="flex justify-between">
<button onclick="showAddOrgStep(2)" class="px-4 py-2 text-[#b0bec5] hover:text-white text-sm transition">Back</button>
<button onclick="submitAddOrg()" id="addOrgSubmitBtn" class="px-5 py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Add to Deal</button>
</div>
</div>
</div>
</div>
<!-- Person Slide-out Panel -->
<div id="personOverlay" class="hidden fixed inset-0 bg-black/40 z-50" onclick="closePersonPanel()"></div>
<div id="personPanel" class="fixed top-0 right-0 h-full w-full max-w-md z-50 transform translate-x-full transition-transform duration-200 ease-out"
style="background:var(--ds-sf, #0d1f3c); border-left: 1px solid var(--ds-bd, rgba(255,255,255,.08));">
<div class="h-full flex flex-col">
<!-- Header -->
<div class="flex items-center justify-between px-6 py-4 border-b" style="border-color:var(--ds-bd)">
<h3 id="personPanelTitle" class="text-lg font-semibold" style="color:var(--ds-tx)">Edit Person</h3>
<button onclick="closePersonPanel()" class="text-2xl leading-none" style="color:var(--ds-tx2)">&times;</button>
</div>
<!-- Body -->
<div class="flex-1 overflow-y-auto min-h-0 px-6 py-5 space-y-4">
<input type="hidden" id="ppDealOrgId">
<input type="hidden" id="ppMemberIdx">
<!-- Photo + Name -->
<div class="flex items-center gap-4">
<div id="ppPhotoWrap" class="w-20 h-20 rounded-full overflow-hidden shrink-0 bg-[#c9a84c]/20 flex items-center justify-center cursor-pointer relative group"
onclick="document.getElementById('ppPhotoUrlRow').classList.toggle('hidden')" title="Click to change photo">
<img id="ppPhotoImg" src="" class="w-full h-full object-cover hidden">
<span id="ppPhotoLetter" class="text-[#c9a84c] text-2xl font-semibold"></span>
<div class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition flex items-center justify-center rounded-full">
<span class="text-white text-xs">Edit</span>
</div>
</div>
<div class="flex-1">
<label class="block text-xs mb-1" style="color:var(--ds-tx2)">Name</label>
<input type="text" id="ppName" class="w-full px-3 py-2 rounded-lg text-sm focus:outline-none"
style="background:var(--ds-inp);border:1px solid var(--ds-bd);color:var(--ds-tx)"
oninput="previewPersonPhoto()">
</div>
</div>
<div id="ppPhotoUrlRow" class="hidden">
<label class="block text-xs mb-1" style="color:var(--ds-tx2)">Photo URL</label>
<input type="text" id="ppPhoto" placeholder="https://..."
class="w-full px-3 py-2 rounded-lg text-sm focus:outline-none"
style="background:var(--ds-inp);border:1px solid var(--ds-bd);color:var(--ds-tx)"
oninput="previewPersonPhoto()">
</div>
<div>
<label class="block text-xs mb-1" style="color:var(--ds-tx2)">Email</label>
<input type="email" id="ppEmail" class="w-full px-3 py-2 rounded-lg text-sm focus:outline-none"
style="background:var(--ds-inp);border:1px solid var(--ds-bd);color:var(--ds-tx)">
</div>
<div>
<label class="block text-xs mb-1" style="color:var(--ds-tx2)">Title</label>
<input type="text" id="ppTitle" class="w-full px-3 py-2 rounded-lg text-sm focus:outline-none"
style="background:var(--ds-inp);border:1px solid var(--ds-bd);color:var(--ds-tx)">
</div>
<div>
<label class="block text-xs mb-1" style="color:var(--ds-tx2)">Phone</label>
<input type="text" id="ppPhone" class="w-full px-3 py-2 rounded-lg text-sm focus:outline-none"
style="background:var(--ds-inp);border:1px solid var(--ds-bd);color:var(--ds-tx)">
</div>
<div>
<label class="block text-xs mb-1" style="color:var(--ds-tx2)">LinkedIn</label>
<input type="text" id="ppLinkedIn" placeholder=""
class="w-full px-3 py-2 rounded-lg text-sm focus:outline-none"
style="background:var(--ds-inp);border:1px solid var(--ds-bd);color:var(--ds-tx)">
</div>
<div>
<label class="block text-xs mb-1" style="color:var(--ds-tx2)">Bio</label>
<textarea id="ppBio" rows="5" class="w-full px-3 py-2 rounded-lg text-sm focus:outline-none resize-none"
style="background:var(--ds-inp);border:1px solid var(--ds-bd);color:var(--ds-tx)"></textarea>
</div>
</div>
<!-- Footer -->
<div class="px-6 py-4 pb-16 border-t flex items-center gap-3 shrink-0" style="border-color:var(--ds-bd)">
<button onclick="removePersonFromDeal()" id="ppRemoveBtn"
class="px-3 py-2 rounded-lg text-sm font-medium transition"
style="background:rgba(239,68,68,.1);color:#f87171;border:1px solid rgba(239,68,68,.2)">Remove</button>
<div class="flex-1"></div>
<button onclick="closePersonPanel()" class="px-4 py-2 rounded-lg text-sm font-medium transition"
style="background:var(--ds-hv);color:var(--ds-tx)">Cancel</button>
<button onclick="savePersonPanel()" class="px-4 py-2 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Save</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-[#b0bec5] 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-[#b0bec5]">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-[#b0bec5]">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-[#b0bec5]">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-[#b0bec5]">Edit request lists</span>
<input type="checkbox" id="pmEditLists" class="w-4 h-4 accent-[#c9a84c]">
</div>
<div>
<div class="flex items-center justify-between mb-2">
<span class="text-sm text-[#b0bec5]">Visible request lists</span>
<label class="flex items-center gap-1.5 text-xs text-[#b0bec5] cursor-pointer">
<input type="checkbox" id="pmAllLists" class="w-3.5 h-3.5 accent-[#c9a84c]" onchange="toggleAllLists(this.checked)">
All lists
</label>
</div>
<div id="pmListChecks" class="space-y-1.5 max-h-40 overflow-y-auto pr-1"></div>
</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:#b0bec5' };
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 ? 'Detailed View' : 'Summary View';
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;
}
// Auto-summarize long request text for Summary View.
// Short text (≤8 words or ≤60 chars) is returned as-is.
// Longer text: extract the first complete sentence; if still long, trim to ~80 chars at a word boundary.
function summarize(text) {
if (!text) return text;
const wordCount = text.trim().split(/\s+/).length;
if (wordCount <= 8 || text.length <= 60) return text;
// Try to find a sentence boundary
const m = text.match(/^(.+?[.!?])(?:\s|$)/);
if (m && m[1].length <= 120) return m[1];
// No clean sentence — trim to ~80 chars at last word boundary
if (text.length <= 80) return text;
const cut = text.lastIndexOf(' ', 80);
return text.substring(0, cut > 20 ? cut : 80) + '…';
}
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="11" 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>
<button onclick="startInlineAdd('section','${item.entry_id}')" style="margin-left:10px;background:none;border:1px solid var(--ds-bd);border-radius:4px;cursor:pointer;color:var(--ds-tx3);font-size:11px;padding:1px 8px">+ Section</button>
<button onclick="startInlineAdd('request','${item.entry_id}')" style="margin-left:4px;background:none;border:1px solid var(--ds-bd);border-radius:4px;cursor:pointer;color:var(--ds-tx3);font-size:11px;padding:1px 8px">+ Request</button>
</td>
<td style="position:relative;text-align:right">
<button onclick="toggleMenu(event,'${item.entry_id}')" style="background:none;border:none;cursor:pointer;color:var(--ds-tx3);font-size:16px;padding:2px 6px">&hellip;</button>
<div id="menu-${item.entry_id}" class="actions-menu" style="display:none">
<button onclick="startInlineAdd('section','${item.entry_id}')">+ Add Section</button>
<button onclick="startInlineAdd('request','${item.entry_id}')">+ Add Request</button>
<button onclick="editVisibility('${item.entry_id}')">Change Visibility</button>
<button onclick="renameListOrSection('${item.entry_id}','list')" style="border-top:1px solid var(--ds-bd);margin-top:2px;padding-top:6px">Rename</button>
<button onclick="deleteEntry('${item.entry_id}')" style="color:#f87171">Delete List</button>
</div>
</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="10" 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>
<button onclick="startInlineAdd('request','${item.entry_id}')" style="margin-left:10px;background:none;border:1px solid var(--ds-bd);border-radius:4px;cursor:pointer;color:var(--ds-tx3);font-size:11px;padding:1px 8px">+ Request</button>
</td>
<td style="position:relative;text-align:right">
<button onclick="toggleMenu(event,'${item.entry_id}')" style="background:none;border:none;cursor:pointer;color:var(--ds-tx3);font-size:16px;padding:2px 6px">&hellip;</button>
<div id="menu-${item.entry_id}" class="actions-menu" style="display:none">
<button onclick="startInlineAdd('request','${item.entry_id}')">+ Add Request</button>
<button onclick="renameListOrSection('${item.entry_id}','section')" style="border-top:1px solid var(--ds-bd);margin-top:2px;padding-top:6px">Rename</button>
<button onclick="deleteEntry('${item.entry_id}')" style="color:#f87171">Delete Section</button>
</div>
</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 {
titleHtml = `<a href="/app/requests/${eid}" style="color:var(--ds-tx);text-decoration:none;line-height:1.4" class="hover:underline">${escHtml(summarize(title))}</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="showAttachModalById('${eid}')">${item.answer_count}</span>`
: `<span style="color:var(--ds-tx3);cursor:pointer;font-size:12px" onclick="showAttachModalById('${eid}')">&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="showAttachModalById('${eid}')">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-[#b0bec5] text-sm py-4">No requests found.</td></tr>';
tbody.innerHTML = html;
}
// ---- Inline add section / request ----
let inlineAddRow = null;
function startInlineAdd(type, parentId) {
// Remove any existing inline row
const existing = document.getElementById('inlineAddRow');
if (existing) existing.remove();
const label = type === 'section' ? 'Section name' : 'Request text';
const placeholder = type === 'section' ? 'e.g. Legal, Financial, Operations' : 'Enter the full request text — Dealspace will automatically show a concise summary in Summary View';
const tr = document.createElement('tr');
tr.id = 'inlineAddRow';
tr.setAttribute('data-parent', parentId);
tr.setAttribute('data-addtype', type);
tr.innerHTML = `
<td></td>
<td colspan="12" style="padding:8px 10px 8px 40px">
<div style="display:flex;align-items:center;gap:8px">
<input id="inlineAddInput" type="text" placeholder="${placeholder}"
style="flex:1;padding:6px 10px;border-radius:6px;border:1px solid var(--ds-ac);background:var(--ds-bg);color:var(--ds-tx);font-size:13px;outline:none"
onkeydown="if(event.key==='Enter')submitInlineAdd();if(event.key==='Escape')cancelInlineAdd()">
<button onclick="submitInlineAdd()" style="padding:5px 14px;border-radius:6px;border:none;background:var(--ds-ac);color:var(--ds-act);font-size:12px;font-weight:600;cursor:pointer">Add</button>
<button onclick="cancelInlineAdd()" style="padding:5px 10px;border-radius:6px;border:1px solid var(--ds-bd);background:none;color:var(--ds-tx3);font-size:12px;cursor:pointer">Cancel</button>
</div>
</td>`;
// Insert after the parent row
const parentRow = document.querySelector(`tr[data-id="${parentId}"]`);
if (parentRow && parentRow.nextSibling) {
parentRow.parentNode.insertBefore(tr, parentRow.nextSibling);
} else if (parentRow) {
parentRow.parentNode.appendChild(tr);
}
document.getElementById('inlineAddInput').focus();
}
function cancelInlineAdd() {
const el = document.getElementById('inlineAddRow');
if (el) el.remove();
}
async function submitInlineAdd() {
const tr = document.getElementById('inlineAddRow');
if (!tr) return;
const type = tr.getAttribute('data-addtype');
const parentId = tr.getAttribute('data-parent');
const text = document.getElementById('inlineAddInput').value.trim();
if (!text) return;
try {
const endpoint = type === 'section'
? `/api/projects/${projectID}/sections`
: `/api/projects/${projectID}/requests/new`;
const res = await fetchAPI(endpoint, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ name: text, title: text, parent_id: parentId })
});
if (!res.ok) throw new Error('Failed');
cancelInlineAdd();
await loadRequestTree();
} catch(e) {
document.getElementById('inlineAddInput').style.borderColor = 'red';
}
}
async function renameListOrSection(id, type) {
// Close the menu
const menu = document.getElementById('menu-' + id);
if (menu) menu.style.display = 'none';
openMenuId = null;
const item = treeData.find(t => t.entry_id === id);
if (!item) return;
const d = item.data || {};
const currentName = d.name || d.title || '';
const newName = prompt('Rename ' + (type === 'list' ? 'list' : 'section') + ':', currentName);
if (!newName || !newName.trim() || newName.trim() === currentName) return;
const updated = { ...d, name: newName.trim(), title: newName.trim() };
item.data = updated;
try {
const res = await fetchAPI('/api/projects/' + projectID + '/entries/' + id, {
method: 'PUT',
body: JSON.stringify({ data: JSON.stringify(updated), version: item.version || 0 })
});
if (res.ok) {
const result = await res.json();
if (result.version) item.version = result.version;
renderTree();
}
} catch(e) {
loadRequestTree();
}
}
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 showAttachModalById(reqId) {
const item = treeData.find(t => t.entry_id === reqId);
const d = item ? (item.data || {}) : {};
const title = d.title || d.description || 'Untitled';
showAttachModal(reqId, title);
}
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-[#b0bec5] 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-[#b0bec5] 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-[#b0bec5] 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-[#b0bec5] 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 = [];
let _dealOrgsMap = {};
async function loadOrgs() {
try {
const res = await fetchAPI('/api/projects/' + projectID + '/orgs');
const orgs = await res.json();
_dealOrgsMap = {};
if (Array.isArray(orgs)) orgs.forEach(o => {
o._raw = { org_id: o.org_id, role: o.role, domain_lock: o.domain_lock, permissions: o.permissions, members: o.members };
_dealOrgsMap[o.deal_org_id] = o;
});
const list = document.getElementById('orgList');
if (!orgs || orgs.length === 0) {
list.innerHTML = '<div class="text-[#b0bec5] 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-[#8899a6] 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>${(() => {
const vl = perms.visible_lists;
if (!vl || vl === 'all') return '📂 All lists';
if (Array.isArray(vl) && vl.length === 0) return '📂 No lists';
if (Array.isArray(vl)) return '📂 ' + vl.length + ' list' + (vl.length > 1 ? 's' : '');
return '📂 All lists';
})()}</span>
</div>
</div>
<button onclick="openPermModalById('${o.deal_org_id}')"
class="px-3 py-1.5 bg-white/[0.05] hover:bg-white/[0.08] text-[#b0bec5] hover:text-white rounded-lg text-xs transition shrink-0">⚙ Permissions</button>
</div>
<div class="border-t border-white/[0.06] px-5 py-3">
<div class="flex flex-wrap gap-3 items-center">
${members.map((m, mi) => `<div class="flex items-center gap-2 cursor-pointer hover:opacity-80 transition" onclick="openPersonPanel('${o.deal_org_id}', ${mi})">
<div class="w-9 h-9 rounded-full shrink-0 overflow-hidden ${m.photo ? '' : 'bg-[#c9a84c]/20 flex items-center justify-center'}">
${m.photo ? `<img src="${escHtml(m.photo)}" class="w-full h-full object-cover" onerror="this.parentElement.className='w-9 h-9 rounded-full shrink-0 overflow-hidden bg-[#c9a84c]/20 flex items-center justify-center';this.replaceWith(Object.assign(document.createElement('span'),{className:'text-[#c9a84c] text-sm font-semibold',textContent:'${(m.name||m.email||'?')[0].toUpperCase()}'}))">` : `<span class="text-[#c9a84c] text-sm font-semibold">${(m.name||m.email||'?')[0].toUpperCase()}</span>`}
</div>
<div>
<div class="text-white text-xs font-medium">${escHtml(m.name||m.email)}</div>
${m.title ? `<div class="text-[#8899a6] text-xs">${escHtml(m.title)}</div>` : ''}
</div>
</div>`).join('')}
<button onclick="openPersonPanel('${o.deal_org_id}', -1)" class="w-9 h-9 rounded-full bg-white/[0.05] hover:bg-white/[0.08] flex items-center justify-center text-[#b0bec5] hover:text-white transition text-lg" title="Add person">+</button>
</div>
</div>
</div>`;
}).join('');
} catch(e) { console.error(e); }
}
// ---- Add Party Modal (3-step scrape flow) ----
let scrapedData = null;
function openAddOrgModal() {
scrapedData = null;
document.getElementById('addOrgEmail').value = '';
document.getElementById('orgRole').value = '';
document.getElementById('scrapeError').classList.add('hidden');
document.getElementById('scrapeLoading').classList.add('hidden');
document.getElementById('scrapeBtn').disabled = false;
requiredFields.forEach(id => document.getElementById(id).classList.remove('field-error'));
showAddOrgStep(1);
document.getElementById('addOrgModal').classList.remove('hidden');
setTimeout(() => document.getElementById('addOrgEmail').focus(), 100);
}
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');
}
function showAddOrgStep(n) {
document.getElementById('addOrgStep1').classList.toggle('hidden', n !== 1);
document.getElementById('addOrgStep2').classList.toggle('hidden', n !== 2);
document.getElementById('addOrgStep3').classList.toggle('hidden', n !== 3);
if (n === 3) renderPeople();
}
const requiredFields = ['orgName', 'orgRole'];
function highlightRequired() {
requiredFields.forEach(id => {
const el = document.getElementById(id);
if (!el.value.trim()) {
el.classList.add('field-error');
} else {
el.classList.remove('field-error');
}
});
}
function validateStep2() {
highlightRequired();
return requiredFields.every(id => document.getElementById(id).value.trim());
}
// Clear red border on input/change
requiredFields.forEach(id => {
const el = document.getElementById(id);
if (el) {
const clear = () => el.classList.remove('field-error');
el.addEventListener('input', clear);
el.addEventListener('change', clear);
}
});
function ensureEmailPerson(email) {
if (!scrapedData) scrapedData = { domain: '', people: [] };
if (!scrapedData.people) scrapedData.people = [];
const found = scrapedData.people.find(p => p.email && p.email.toLowerCase() === email.toLowerCase());
if (!found) {
scrapedData.people.unshift({ name: '', email: email, title: '', phone: '', photo: '', bio: '', linkedin: '' });
}
}
function skipScrape() {
const email = document.getElementById('addOrgEmail').value.trim();
const domain = email.includes('@') ? email.split('@')[1] : '';
scrapedData = { domain, people: [{ name: '', email: email, title: '', phone: '', photo: '', bio: '', linkedin: '' }] };
document.getElementById('orgLogoWrap').classList.add('hidden');
document.getElementById('orgName').value = '';
document.getElementById('orgDesc').value = '';
document.getElementById('orgIndustry').value = '';
document.getElementById('orgWebsite').value = domain ? 'https://' + domain : '';
document.getElementById('orgPhone').value = '';
document.getElementById('orgAddress').value = '';
document.getElementById('orgCity').value = '';
document.getElementById('orgState').value = '';
document.getElementById('orgFounded').value = '';
document.getElementById('orgLinkedIn').value = '';
highlightRequired();
showAddOrgStep(2);
}
async function scrapeOrg() {
const email = document.getElementById('addOrgEmail').value.trim();
if (!email || !email.includes('@')) {
document.getElementById('scrapeErrorMsg').textContent = 'Enter a valid email address.';
document.getElementById('scrapeError').classList.remove('hidden');
return;
}
document.getElementById('scrapeError').classList.add('hidden');
document.getElementById('scrapeLoading').classList.remove('hidden');
document.getElementById('scrapeBtn').disabled = true;
try {
const res = await fetchAPI('/api/scrape/org', { method: 'POST', body: JSON.stringify({ email }) });
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Scrape failed');
scrapedData = data;
if (data.logo) {
document.getElementById('orgLogoImg').src = data.logo;
document.getElementById('orgLogoWrap').classList.remove('hidden');
} else {
document.getElementById('orgLogoWrap').classList.add('hidden');
}
document.getElementById('orgName').value = data.name || '';
document.getElementById('orgDesc').value = data.description || '';
document.getElementById('orgIndustry').value = data.industry || '';
document.getElementById('orgWebsite').value = data.website || '';
document.getElementById('orgPhone').value = data.phone || '';
document.getElementById('orgAddress').value = data.address || '';
document.getElementById('orgCity').value = data.city || '';
document.getElementById('orgState').value = data.state || '';
document.getElementById('orgFounded').value = data.founded || '';
document.getElementById('orgLinkedIn').value = data.linkedin || '';
// Ensure the original email person is in the people list
ensureEmailPerson(email);
highlightRequired();
showAddOrgStep(2);
} catch (err) {
document.getElementById('scrapeErrorMsg').textContent = err.message;
document.getElementById('scrapeError').classList.remove('hidden');
} finally {
document.getElementById('scrapeLoading').classList.add('hidden');
document.getElementById('scrapeBtn').disabled = false;
}
}
function renderPeople() {
const people = scrapedData?.people || [];
const list = document.getElementById('peopleList');
const none = document.getElementById('noPeople');
if (people.length === 0) { list.innerHTML = ''; none.classList.remove('hidden'); return; }
none.classList.add('hidden');
list.innerHTML = people.map((p, i) => {
const hasDetail = p.name && p.title;
return `
<div class="flex items-center gap-3 p-3 rounded-xl bg-white/[0.03] border border-white/[0.06] transition">
<input type="checkbox" class="person-cb accent-[#c9a84c] rounded shrink-0" data-idx="${i}" checked onchange="updateSelectedCount()">
<div class="w-10 h-10 rounded-full bg-[#0a1628] shrink-0 overflow-hidden border border-white/[0.08]">
${p.photo ? `<img src="${escHtml(p.photo)}" class="w-full h-full object-cover" onerror="this.parentElement.innerHTML='<div class=\\'w-full h-full flex items-center justify-center text-[#c9a84c] font-semibold\\'>${escHtml((p.name||'?')[0])}</div>'">` : `<div class="w-full h-full flex items-center justify-center text-[#c9a84c] font-semibold">${escHtml((p.name||'?')[0])}</div>`}
</div>
<div class="flex-1 min-w-0 grid grid-cols-3 gap-2">
<input type="text" value="${escHtml(p.name)}" placeholder="Name" data-idx="${i}" data-field="name" onchange="updatePerson(this)"
class="px-2 py-1 bg-transparent border border-transparent hover:border-white/[0.08] focus:border-[#c9a84c] rounded text-white text-sm focus:outline-none ${hasDetail ? '' : 'border-white/[0.08]'}">
<input type="text" value="${escHtml(p.email)}" placeholder="Email" data-idx="${i}" data-field="email" onchange="updatePerson(this)"
class="px-2 py-1 bg-transparent border border-transparent hover:border-white/[0.08] focus:border-[#c9a84c] rounded text-[#b0bec5] text-xs focus:outline-none ${hasDetail ? '' : 'border-white/[0.08]'}">
<input type="text" value="${escHtml(p.title)}" placeholder="Title" data-idx="${i}" data-field="title" onchange="updatePerson(this)"
class="px-2 py-1 bg-transparent border border-transparent hover:border-white/[0.08] focus:border-[#c9a84c] rounded text-[#b0bec5] text-xs focus:outline-none ${hasDetail ? '' : 'border-white/[0.08]'}">
</div>
</div>`;
}).join('');
document.getElementById('selectAllPeople').checked = true;
updateSelectedCount();
}
function updatePerson(el) {
const idx = parseInt(el.dataset.idx);
const field = el.dataset.field;
if (scrapedData?.people?.[idx]) {
scrapedData.people[idx][field] = el.value.trim();
}
}
function addManualPerson() {
const name = document.getElementById('manualName').value.trim();
const email = document.getElementById('manualEmail').value.trim();
const title = document.getElementById('manualTitle').value.trim();
if (!name && !email) return;
if (!scrapedData) scrapedData = { domain: '', people: [] };
scrapedData.people.push({ name: name || email, email, title, phone: '', photo: '', bio: '', linkedin: '' });
document.getElementById('manualName').value = '';
document.getElementById('manualEmail').value = '';
document.getElementById('manualTitle').value = '';
renderPeople();
document.getElementById('manualName').focus();
}
function toggleAllPeople(checked) {
document.querySelectorAll('.person-cb').forEach(cb => cb.checked = checked);
updateSelectedCount();
}
function updateSelectedCount() {
const checked = document.querySelectorAll('.person-cb:checked').length;
const total = document.querySelectorAll('.person-cb').length;
document.getElementById('selectedCount').textContent = checked + ' of ' + total + ' selected';
document.getElementById('selectAllPeople').checked = (checked === total);
}
async function submitAddOrg() {
if (!validateStep2()) { showAddOrgStep(2); return; }
const name = document.getElementById('orgName').value.trim();
const role = document.getElementById('orgRole').value;
const domain = scrapedData?.domain || '';
const people = scrapedData?.people || [];
const selectedMembers = [];
document.querySelectorAll('.person-cb:checked').forEach(cb => {
const p = people[parseInt(cb.dataset.idx)];
if (p) selectedMembers.push({ name: p.name, email: p.email || '', title: p.title || '', phone: p.phone || '', photo: p.photo || '', bio: p.bio || '', linkedin: p.linkedin || '' });
});
const btn = document.getElementById('addOrgSubmitBtn');
btn.disabled = true; btn.textContent = 'Adding...';
try {
const res = await fetchAPI('/api/projects/' + projectID + '/orgs/add', {
method: 'POST',
body: JSON.stringify({
name,
domains: domain ? [domain] : [],
role,
website: document.getElementById('orgWebsite').value,
description: document.getElementById('orgDesc').value,
industry: document.getElementById('orgIndustry').value,
phone: document.getElementById('orgPhone').value,
address: document.getElementById('orgAddress').value,
city: document.getElementById('orgCity').value,
state: document.getElementById('orgState').value,
founded: document.getElementById('orgFounded').value,
linkedin: document.getElementById('orgLinkedIn').value,
members: selectedMembers,
domain_lock: true,
})
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to add organization');
closeAddOrgModal();
loadOrgs();
} catch (err) { alert(err.message); }
finally { btn.disabled = false; btn.textContent = 'Add to Deal'; }
}
// ---- Person Panel ----
function openPersonPanel(dealOrgId, memberIdx) {
const o = _dealOrgsMap[dealOrgId];
if (!o) return;
const members = o.members || [];
const isNew = memberIdx < 0;
const m = isNew ? { name: '', email: '', title: '', phone: '', photo: '', bio: '', linkedin: '' } : members[memberIdx];
if (!m) return;
document.getElementById('ppDealOrgId').value = dealOrgId;
document.getElementById('ppMemberIdx').value = memberIdx;
document.getElementById('ppName').value = m.name || '';
document.getElementById('ppEmail').value = m.email || '';
document.getElementById('ppTitle').value = m.title || '';
document.getElementById('ppPhone').value = m.phone || '';
document.getElementById('ppPhoto').value = m.photo || '';
document.getElementById('ppLinkedIn').value = m.linkedin || '';
document.getElementById('ppBio').value = m.bio || '';
document.getElementById('personPanelTitle').textContent = isNew ? 'Add Person' : 'Edit Person';
document.getElementById('ppRemoveBtn').classList.toggle('hidden', isNew);
previewPersonPhoto();
document.getElementById('ppPhotoUrlRow').classList.toggle('hidden', !!m.photo);
document.getElementById('personOverlay').classList.remove('hidden');
document.getElementById('personPanel').classList.remove('translate-x-full');
}
function closePersonPanel() {
document.getElementById('personPanel').classList.add('translate-x-full');
document.getElementById('personOverlay').classList.add('hidden');
}
function previewPersonPhoto() {
const url = document.getElementById('ppPhoto').value.trim();
const img = document.getElementById('ppPhotoImg');
const letter = document.getElementById('ppPhotoLetter');
if (url) {
img.src = url;
img.classList.remove('hidden');
img.onerror = () => { img.classList.add('hidden'); letter.classList.remove('hidden'); };
letter.classList.add('hidden');
} else {
img.classList.add('hidden');
letter.classList.remove('hidden');
const name = document.getElementById('ppName').value || document.getElementById('ppEmail').value || '?';
letter.textContent = name[0].toUpperCase();
}
}
async function savePersonPanel() {
const dealOrgId = document.getElementById('ppDealOrgId').value;
const memberIdx = parseInt(document.getElementById('ppMemberIdx').value);
const o = _dealOrgsMap[dealOrgId];
if (!o) return;
const person = {
name: document.getElementById('ppName').value.trim(),
email: document.getElementById('ppEmail').value.trim(),
title: document.getElementById('ppTitle').value.trim(),
phone: document.getElementById('ppPhone').value.trim(),
photo: document.getElementById('ppPhoto').value.trim(),
linkedin: document.getElementById('ppLinkedIn').value.trim(),
bio: document.getElementById('ppBio').value.trim(),
};
if (!person.name && !person.email) return;
const members = [...(o.members || [])];
if (memberIdx < 0) {
members.push(person);
} else {
members[memberIdx] = person;
}
try {
await fetchAPI('/api/projects/' + projectID + '/entries/' + dealOrgId, {
method: 'PUT',
body: JSON.stringify({
data: JSON.stringify({ ...o._raw, members }),
version: o.version || 1
})
});
closePersonPanel();
loadOrgs();
} catch (e) { alert(e.message); }
}
async function removePersonFromDeal() {
const dealOrgId = document.getElementById('ppDealOrgId').value;
const memberIdx = parseInt(document.getElementById('ppMemberIdx').value);
const o = _dealOrgsMap[dealOrgId];
if (!o || memberIdx < 0) return;
const name = (o.members || [])[memberIdx]?.name || 'this person';
if (!confirm('Remove ' + name + ' from this deal?')) return;
const members = [...(o.members || [])];
members.splice(memberIdx, 1);
try {
await fetchAPI('/api/projects/' + projectID + '/entries/' + dealOrgId, {
method: 'PUT',
body: JSON.stringify({
data: JSON.stringify({ ...o._raw, members }),
version: o.version || 1
})
});
closePersonPanel();
loadOrgs();
} catch (e) { alert(e.message); }
}
// ESC to close person panel
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && !document.getElementById('personPanel').classList.contains('translate-x-full')) {
closePersonPanel();
}
});
// ---- Permissions Modal ----
function openPermModalById(dealOrgId) {
const o = _dealOrgsMap[dealOrgId];
if (!o) return;
openPermModal(o.deal_org_id, o.org_id, o.role, o.version || 1, JSON.stringify(o.permissions || {}));
}
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;
// Populate request list checkboxes
const visibleLists = perms.visible_lists || null; // null = all
const allChecked = visibleLists === null || visibleLists === 'all';
document.getElementById('pmAllLists').checked = allChecked;
const lists = treeData.filter(t => t.type === 'request_list');
const container = document.getElementById('pmListChecks');
container.innerHTML = lists.map(l => {
const name = (l.data && l.data.name) ? l.data.name : 'Request List';
const checked = allChecked || (Array.isArray(visibleLists) && visibleLists.includes(l.entry_id));
return `<label class="flex items-center gap-2 text-sm text-white cursor-pointer">
<input type="checkbox" class="pm-list-check w-3.5 h-3.5 accent-[#c9a84c]" value="${l.entry_id}" ${checked ? 'checked' : ''}>
${escHtml(name)}
</label>`;
}).join('');
container.style.display = allChecked ? 'none' : 'block';
document.getElementById('permModal').classList.remove('hidden');
}
function closePermModal() { document.getElementById('permModal').classList.add('hidden'); }
function toggleAllLists(checked) {
const container = document.getElementById('pmListChecks');
container.style.display = checked ? 'none' : 'block';
document.querySelectorAll('.pm-list-check').forEach(cb => cb.checked = checked);
}
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,
visible_lists: document.getElementById('pmAllLists').checked
? 'all'
: Array.from(document.querySelectorAll('.pm-list-check:checked')).map(cb => cb.value),
};
try {
await fetchAPI('/api/projects/' + projectID + '/entries/' + dealOrgId, {
method: 'PUT',
body: JSON.stringify({
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-[#b0bec5]'); });
el.classList.add('active','text-white'); el.classList.remove('text-[#b0bec5]');
document.getElementById('tab-requests').classList.toggle('hidden', name !== 'requests');
document.getElementById('tab-orgs').classList.toggle('hidden', name !== 'orgs');
if (name === 'orgs') loadOrgs();
}
// ---- Import ----
function openCreateListModal() {
document.getElementById('createListName').value = '';
document.getElementById('createListError').classList.add('hidden');
document.getElementById('createListModal').style.display = 'flex';
setTimeout(() => document.getElementById('createListName').focus(), 50);
}
function closeCreateListModal() { document.getElementById('createListModal').style.display = 'none'; }
async function submitCreateList() {
const name = document.getElementById('createListName').value.trim();
if (!name) return;
const btn = document.getElementById('createListBtn');
btn.disabled = true; btn.textContent = 'Creating...';
try {
const res = await fetchAPI('/api/projects/' + projectID + '/requests', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name})
});
if (!res.ok) { const e = await res.json(); throw new Error(e.message || 'Failed'); }
closeCreateListModal();
await loadRequestTree();
} catch(e) {
document.getElementById('createListError').textContent = e.message;
document.getElementById('createListError').classList.remove('hidden');
} finally {
btn.disabled = false; btn.textContent = 'Create';
}
}
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}}