dealroom/templates/dealroom.templ

1056 lines
59 KiB
Plaintext

package templates
import "dealroom/internal/model"
import "fmt"
import "strings"
import "time"
templ DealRoomDetail(profile *model.Profile, deal *model.Deal, folders []*model.Folder, files []*model.File, requests []*model.DiligenceRequest, activeFolder string) {
@Layout(profile, "deals") {
<div class="space-y-5">
<!-- Header -->
<div>
<a href="/deals" class="text-sm text-gray-500 hover:text-gray-300 flex items-center gap-1 mb-3">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
Back to Deal Rooms
</a>
<div class="flex items-center gap-3">
<h1 class="text-2xl font-bold">{ deal.Name }</h1>
@StageBadge(deal.Stage)
if profile.Role == "owner" || profile.Role == "admin" {
<button onclick="document.getElementById('editDealModal').classList.remove('hidden')" class="text-xs text-gray-500 hover:text-teal-400 transition ml-1" title="Edit deal info">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
</button>
}
</div>
<p class="text-sm text-gray-500 mt-1">{ deal.TargetCompany } · { deal.Description }</p>
</div>
<!-- Search -->
<div class="relative">
<input type="text" placeholder="Search files, folders, requests..."
hx-get={ fmt.Sprintf("/deals/search/%s", deal.ID) }
hx-trigger="input changed delay:300ms"
hx-target="#searchResults"
name="q"
class="w-full px-4 py-2 bg-gray-900 border border-gray-800 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none placeholder-gray-600"/>
<div id="searchResults" class="mt-2"></div>
</div>
<!-- Deal Info Cards -->
<div class="grid grid-cols-3 gap-4">
<div class="bg-gray-900 rounded-lg border border-gray-800 p-4">
<div class="text-xs text-gray-500 uppercase tracking-wider mb-1">Deal Size</div>
<div class="text-lg font-bold">{ formatDealSize(deal.DealSize) }</div>
</div>
<div class="bg-gray-900 rounded-lg border border-gray-800 p-4">
<div class="text-xs text-gray-500 uppercase tracking-wider mb-1">IOI Date</div>
<div class="text-lg font-bold">
if deal.IOIDate != "" {
{ deal.IOIDate }
} else {
<span class="text-gray-600">&mdash;</span>
}
</div>
</div>
<div class="bg-gray-900 rounded-lg border border-gray-800 p-4">
<div class="text-xs text-gray-500 uppercase tracking-wider mb-1">Exclusivity Ends</div>
<div class="text-lg font-bold">
if deal.ExclusivityEnd != "" {
{ deal.ExclusivityEnd }
} else {
<span class="text-gray-600">&mdash;</span>
}
</div>
</div>
</div>
<!-- Tabs -->
<div>
<div class="flex gap-1 border-b border-gray-800 mb-4">
<button onclick="showTab('documents')" id="tab-documents" class="px-4 py-2 text-sm font-medium border-b-2 border-teal-500 text-teal-400">
Documents ({ fmt.Sprintf("%d", len(files)) })
</button>
<button onclick="showTab('requests')" id="tab-requests" class="px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-300">
Request List ({ fmt.Sprintf("%d", len(requests)) })
</button>
</div>
<!-- Documents Tab -->
<div id="panel-documents">
<div class="grid grid-cols-4 gap-4">
<!-- Folder Tree -->
<div class="bg-gray-900 rounded-lg border border-gray-800 p-3">
<h3 class="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">Folders</h3>
<!-- All Documents -->
<div class="mb-1">
<a href={ templ.SafeURL(fmt.Sprintf("/deals/%s?folder=all", deal.ID)) } class={ "flex items-center gap-1.5 py-1.5 px-2 rounded text-sm", templ.KV("bg-teal-500/10 text-teal-400", activeFolder == "all"), templ.KV("hover:bg-gray-800 text-gray-300", activeFolder != "all") }>
<svg class="w-4 h-4 text-teal-400/70 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"></path></svg>
<span class="truncate">All Documents</span>
</a>
</div>
<div id="folderList">
for _, folder := range folders {
if folder.ParentID == "" {
<div class="mb-1 folder-item" draggable="true"
data-folder-id={ folder.ID } data-folder-parent="" data-deal-id={ deal.ID }
ondragstart="handleFolderDragStart(event)" ondragend="handleFolderDragEnd(event)"
ondragover="handleFolderDragOver(event)" ondragleave="handleFolderDragLeave(event)" ondrop="handleFolderDrop(event)"
oncontextmenu={ templ.ComponentScript{Call: fmt.Sprintf("showFolderContextMenu(event, '%s', '%s')", folder.ID, deal.ID)} }>
<a href={ templ.SafeURL(fmt.Sprintf("/deals/%s?folder=%s", deal.ID, folder.ID)) } class={ "flex items-center gap-1.5 py-1.5 px-2 rounded text-sm cursor-pointer transition-colors", templ.KV("bg-teal-500/10 text-teal-400", activeFolder == folder.ID), templ.KV("hover:bg-gray-800 text-gray-300", activeFolder != folder.ID) }>
<svg class="w-3 h-3 text-gray-600 cursor-grab shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16"></path></svg>
<svg class="w-4 h-4 text-teal-400/70 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path></svg>
<span class="truncate">{ folder.Name }</span>
</a>
<!-- Child folders (now draggable + reparentable) -->
for _, child := range folders {
if child.ParentID == folder.ID {
<div class="ml-4 folder-item" draggable="true"
data-folder-id={ child.ID } data-folder-parent={ folder.ID } data-deal-id={ deal.ID }
ondragstart="handleFolderDragStart(event)" ondragend="handleFolderDragEnd(event)"
ondragover="handleFolderDragOver(event)" ondragleave="handleFolderDragLeave(event)" ondrop="handleFolderDrop(event)"
oncontextmenu={ templ.ComponentScript{Call: fmt.Sprintf("showFolderContextMenu(event, '%s', '%s')", child.ID, deal.ID)} }>
<a href={ templ.SafeURL(fmt.Sprintf("/deals/%s?folder=%s", deal.ID, child.ID)) } class={ "flex items-center gap-1.5 py-1.5 px-2 rounded text-sm cursor-pointer transition-colors", templ.KV("bg-teal-500/10 text-teal-400", activeFolder == child.ID), templ.KV("hover:bg-gray-800 text-gray-300", activeFolder != child.ID) }>
<svg class="w-3 h-3 text-gray-600 cursor-grab shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16"></path></svg>
<svg class="w-4 h-4 text-teal-400/70 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path></svg>
<span class="truncate">{ child.Name }</span>
</a>
</div>
}
}
</div>
}
}
</div>
<!-- Root drop zone: drag a subfolder here to promote it to root level -->
<div id="rootDropZone" class="hidden mt-2 py-2 px-2 border-2 border-dashed border-teal-500/40 rounded-lg text-center text-xs text-teal-400/60 transition-colors"
ondragover="handleRootZoneDragOver(event)" ondragleave="handleRootZoneDragLeave(event)" ondrop="handleRootZoneDrop(event)">
↑ Drop here to make root-level folder
</div>
if profile.Role != "buyer" {
<div class="mt-4 pt-3 border-t border-gray-800">
<button onclick="document.getElementById('newFolderModal').classList.remove('hidden')" class="w-full flex items-center justify-center gap-1.5 py-1.5 px-2 rounded text-sm text-gray-400 hover:text-teal-400 hover:bg-gray-800 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
New Folder
</button>
</div>
}
</div>
<!-- Files Table -->
<div id="filesPanel" class="col-span-3 bg-gray-900 rounded-lg border border-gray-800"
ondragover="handleFileDragOver(event)" ondrop="handleFileDrop(event)" ondragleave="handleFileDragLeave(event)">
<!-- Toolbar -->
<div class="p-3 border-b border-gray-800 flex gap-3">
<input type="text" id="fileSearch" placeholder="Search files..." oninput="filterFiles()" class="flex-1 px-3 py-1.5 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
<button onclick="openUploadModal()" class="px-3 py-1.5 bg-teal-600 hover:bg-teal-500 text-white text-sm font-medium rounded-lg transition-colors flex items-center gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg>
Upload Document
</button>
</div>
<!-- Drop zone overlay -->
<div id="dropOverlay" class="hidden p-8 text-center border-2 border-dashed border-teal-500/50 m-2 rounded-lg bg-teal-500/5">
<svg class="w-10 h-10 text-teal-400 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path></svg>
<p class="text-sm text-teal-400 font-medium">Drop files here to upload</p>
</div>
<table class="w-full">
<thead>
<tr class="border-b border-gray-800">
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider">File Name</th>
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider w-24">Size</th>
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider w-24">Date</th>
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider w-28">Status</th>
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider w-16">AI</th>
<th class="text-right px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider w-20">Actions</th>
</tr>
</thead>
<tbody id="fileTableBody" class="divide-y divide-gray-800/50">
for _, file := range files {
if activeFolder == "" || activeFolder == "all" || file.FolderID == activeFolder {
<tr class="hover:bg-gray-800/30 transition file-row" data-filename={ file.Name }>
<td class="px-4 py-2.5">
<div class="flex items-center gap-2">
@fileIcon(file.Name)
<span class="text-sm">{ file.Name }</span>
</div>
</td>
<td class="px-4 py-2.5 text-xs text-gray-500">{ formatFileSize(file.FileSize) }</td>
<td class="px-4 py-2.5 text-xs text-gray-500">{ formatFileDate(file.CreatedAt) }</td>
<td class="px-4 py-2.5">
<div class="relative status-dropdown">
<button onclick={ templ.ComponentScript{Call: fmt.Sprintf("toggleStatusDropdown(event, '%s', '%s')", deal.ID, file.ID)} } class="cursor-pointer">
@FileStatusBadge(file.Status)
</button>
<div class="hidden absolute z-10 mt-1 right-0 w-36 bg-gray-800 border border-gray-700 rounded-lg shadow-xl py-1" data-dropdown={ file.ID }>
@statusOption(deal.ID, file.ID, "uploaded", "Uploaded")
@statusOption(deal.ID, file.ID, "processing", "Processing")
@statusOption(deal.ID, file.ID, "reviewed", "Reviewed")
@statusOption(deal.ID, file.ID, "flagged", "Flagged")
@statusOption(deal.ID, file.ID, "archived", "Archived")
</div>
</div>
</td>
<td class="px-4 py-2.5 text-center extraction-status" data-status={ file.ExtractionStatus }>
if file.ExtractionStatus == "pending" || file.ExtractionStatus == "processing" {
<span class="text-amber-400 text-xs" title="Extracting...">&#9203;</span>
} else if file.ExtractionStatus == "done" {
<span class="text-green-500 text-xs" title="Extracted">&#10003;</span>
} else if file.ExtractionStatus == "failed" {
<span class="text-red-400 text-xs" title="Extraction failed">&#9888;</span>
}
</td>
<td class="px-4 py-2.5 text-right">
<div class="flex items-center justify-end gap-2">
<button hx-get={ fmt.Sprintf("/deals/files/comments/%s", file.ID) } hx-target={ fmt.Sprintf("#comments-%s", file.ID) } hx-swap="innerHTML" onclick={ templ.ComponentScript{Call: fmt.Sprintf("document.getElementById('commentPanel-%s').classList.toggle('hidden')", file.ID)} } class="text-xs text-gray-500 hover:text-teal-400 transition" title="Comments">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path></svg>
</button>
<a href={ templ.SafeURL(fmt.Sprintf("/deals/files/download/%s", file.ID)) } class="text-xs text-gray-500 hover:text-teal-400 transition" title="Download">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
</a>
if profile.Role == "owner" || profile.Role == "admin" {
<form action="/deals/files/delete" method="POST" style="display:inline" onsubmit="return confirm('Delete this file?')">
<input type="hidden" name="file_id" value={ file.ID }/>
<input type="hidden" name="deal_id" value={ deal.ID }/>
<button type="submit" class="text-xs text-gray-500 hover:text-red-400 transition" title="Delete">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
</button>
</form>
}
</div>
</td>
</tr>
<tr id={ fmt.Sprintf("commentPanel-%s", file.ID) } class="hidden">
<td colspan="6" class="px-4 py-3 bg-gray-800/30">
<div id={ fmt.Sprintf("comments-%s", file.ID) }>
<p class="text-xs text-gray-500 italic">Click the comment icon to load comments.</p>
</div>
</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</div>
<!-- Requests Tab -->
<div id="panel-requests" style="display:none">
<!-- Toolbar: Upload, Add Statement, Assignment Rules -->
if profile.Role == "owner" || profile.Role == "admin" {
<div class="mb-4 flex items-center justify-between">
<div class="flex items-center gap-2">
<button onclick="document.getElementById('assignmentRulesPanel').classList.toggle('hidden')" class="px-3 py-1.5 border border-gray-700 text-gray-400 hover:text-teal-400 hover:border-teal-500 text-sm rounded-lg transition-colors flex items-center gap-1.5" title="Assignment Rules">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
Rules
</button>
</div>
<div class="flex items-center gap-2">
<button onclick="document.getElementById('addStatementModal').classList.remove('hidden')" class="px-3 py-1.5 border border-gray-700 text-gray-400 hover:text-teal-400 hover:border-teal-500 text-sm rounded-lg transition-colors flex items-center gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
Add Statement
</button>
<button onclick="document.getElementById('uploadRequestListModal').classList.remove('hidden')" class="px-3 py-1.5 bg-teal-600 hover:bg-teal-500 text-white text-sm font-medium rounded-lg transition-colors flex items-center gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg>
Upload Request List
</button>
</div>
</div>
}
<!-- Assignment Rules Panel (hidden by default) -->
<div id="assignmentRulesPanel" class="hidden mb-4 bg-gray-900 rounded-lg border border-gray-800 p-4">
<h3 class="text-sm font-medium text-gray-300 mb-3">Assignment Rules</h3>
<p class="text-xs text-gray-500 mb-3">Requests matching a keyword in their section/description will be auto-assigned to the specified team member.</p>
<div id="rulesTableBody" class="space-y-2 mb-3">
<div class="text-xs text-gray-500 italic">Loading...</div>
</div>
<div class="flex gap-2">
<input type="text" id="newRuleKeyword" placeholder="Keyword (e.g. Legal)" class="flex-1 px-3 py-1.5 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
<input type="text" id="newRuleAssignee" placeholder="Assignee profile ID" class="flex-1 px-3 py-1.5 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
<button onclick="addAssignmentRule()" class="px-3 py-1.5 bg-gray-800 border border-gray-700 text-teal-400 text-sm rounded-lg hover:bg-gray-700 transition">Add</button>
</div>
<div class="flex justify-end mt-3">
<button onclick="saveAssignmentRules()" class="px-4 py-1.5 bg-teal-600 hover:bg-teal-500 text-white text-sm font-medium rounded-lg transition-colors">Save Rules</button>
</div>
</div>
<!-- Pending AI matches panel -->
<div id="pendingMatchesPanel" class="hidden mb-4 bg-gray-900 rounded-lg border border-teal-500/30 p-4">
<h3 class="text-sm font-medium text-teal-400 mb-3 flex items-center gap-2">
<span>AI-suggested matches waiting for review</span>
</h3>
<div id="pendingMatchesList" class="space-y-2">
</div>
</div>
<div class="bg-gray-900 rounded-lg border border-gray-800">
<table class="w-full">
<thead>
<tr class="border-b border-gray-800">
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider w-16">#</th>
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider w-24">Section</th>
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider">Request Item</th>
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider w-20">Status</th>
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider w-24">Assignee</th>
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider w-16">Priority</th>
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider w-20">Atlas</th>
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider">Seller</th>
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider">Files / AI</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-800/50">
for _, req := range requests {
<tr class="hover:bg-gray-800/30">
<td class="px-4 py-2.5 text-xs text-gray-500">{ req.ItemNumber }</td>
<td class="px-4 py-2.5 text-xs text-gray-400">{ req.Section }</td>
<td class="px-4 py-2.5 text-sm">{ req.Description }</td>
<td class="px-4 py-2.5">
@requestStatusPill(req.Status, req.ConfirmedLinks)
</td>
<td class="px-4 py-2.5 text-xs">
if req.AssigneeName != "" {
<span class="text-gray-300">{ req.AssigneeName }</span>
} else {
<span class="text-gray-600">Unassigned</span>
}
</td>
<td class="px-4 py-2.5">@PriorityBadge(req.Priority)</td>
<td class="px-4 py-2.5">@StatusIcon(req.AtlasStatus)</td>
<td class="px-4 py-2.5 text-xs text-gray-400 max-w-[140px] truncate">{ req.SellerComment }</td>
<td class="px-4 py-2.5">
<div class="flex gap-1 flex-wrap items-center">
if req.LinkedFileIDs != "" {
for _, lfid := range splitLinkedFiles(req.LinkedFileIDs) {
<a href={ templ.SafeURL(fmt.Sprintf("/deals/files/download/%s", lfid)) } class="text-xs px-1.5 py-0.5 rounded bg-teal-500/10 text-teal-400 hover:bg-teal-500/20 transition">
{ getLinkedFileName(files, lfid) }
</a>
}
}
if req.PendingMatches > 0 {
<span class="text-xs px-2 py-0.5 rounded-full border border-teal-500/40 text-teal-400 cursor-pointer hover:bg-teal-500/10 transition" onclick="showTab('requests'); loadPendingMatches()">
{ fmt.Sprintf("%d AI match", req.PendingMatches) }
if req.PendingMatches > 1 {
es
}
</span>
}
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
<!-- Edit Deal Modal -->
<div id="editDealModal" class="hidden fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/60" onclick="document.getElementById('editDealModal').classList.add('hidden')"></div>
<div class="relative bg-gray-900 border border-gray-700 rounded-xl shadow-2xl w-full max-w-lg mx-4 p-6">
<div class="flex items-center justify-between mb-5">
<h2 class="text-lg font-bold">Edit Deal Info</h2>
<button onclick="document.getElementById('editDealModal').classList.add('hidden')" class="text-gray-500 hover:text-gray-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
<form action="/deals/update" method="POST" class="space-y-4 max-h-[70vh] overflow-y-auto pr-1">
<input type="hidden" name="deal_id" value={ deal.ID }/>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Project Name</label>
<input type="text" name="name" value={ deal.Name } required class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
</div>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Company Name</label>
<input type="text" name="target_company" value={ deal.TargetCompany } class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Stage</label>
<select name="stage" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none">
<option value="prospect" selected?={ deal.Stage == "prospect" || deal.Stage == "pipeline" }>Prospect</option>
<option value="internal" selected?={ deal.Stage == "internal" }>Internal</option>
<option value="initial_marketing" selected?={ deal.Stage == "initial_marketing" || deal.Stage == "initial_review" }>Initial Marketing</option>
<option value="ioi" selected?={ deal.Stage == "ioi" || deal.Stage == "due_diligence" }>IOI</option>
<option value="loi" selected?={ deal.Stage == "loi" || deal.Stage == "final_negotiation" }>LOI</option>
<option value="closed" selected?={ deal.Stage == "closed" }>Closed</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Industry</label>
<input type="text" name="industry" value={ deal.Industry } class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Deal Size</label>
<input type="number" name="deal_size" value={ fmt.Sprintf("%.0f", deal.DealSize) } step="0.01" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
</div>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Currency</label>
<select name="currency" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none">
<option value="USD" selected?={ deal.Currency == "USD" }>USD</option>
<option value="EUR" selected?={ deal.Currency == "EUR" }>EUR</option>
<option value="GBP" selected?={ deal.Currency == "GBP" }>GBP</option>
</select>
</div>
</div>
<div class="grid grid-cols-3 gap-3">
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">IOI Date</label>
<input type="date" name="ioi_date" value={ deal.IOIDate } class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
</div>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">LOI Date</label>
<input type="date" name="loi_date" value={ deal.LOIDate } class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
</div>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Exclusivity End</label>
<input type="date" name="exclusivity_end" value={ deal.ExclusivityEnd } class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Description</label>
<textarea name="description" rows="2" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none resize-none">{ deal.Description }</textarea>
</div>
<div class="flex justify-end gap-3 pt-2">
<button type="button" onclick="document.getElementById('editDealModal').classList.add('hidden')" class="px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-gray-200 border border-gray-700 hover:border-gray-600 transition">Cancel</button>
<button type="submit" class="px-4 py-2 rounded-lg bg-teal-500 text-white text-sm font-medium hover:bg-teal-600 transition">Save Changes</button>
</div>
</form>
</div>
</div>
<!-- Upload Modal -->
<div id="uploadModal" class="hidden fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/60" onclick="document.getElementById('uploadModal').classList.add('hidden')"></div>
<div class="relative bg-gray-900 border border-gray-700 rounded-xl shadow-2xl w-full max-w-md mx-4 p-6">
<h2 class="text-lg font-bold mb-4">Upload Document</h2>
<form id="uploadForm" action="/deals/files/upload" method="POST" enctype="multipart/form-data" class="space-y-4">
<input type="hidden" name="deal_id" value={ deal.ID }/>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">File</label>
<div id="uploadDropZone" class="border-2 border-dashed border-gray-700 rounded-lg p-4 text-center hover:border-teal-500/50 transition cursor-pointer" onclick="document.getElementById('fileInput').click()">
<svg class="w-6 h-6 text-gray-500 mx-auto mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path></svg>
<p class="text-sm text-gray-400">Click or drag and drop files</p>
<p id="uploadFileName" class="text-xs text-teal-400 mt-1 hidden"></p>
</div>
<input type="file" id="fileInput" name="file" required class="hidden"/>
</div>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Folder</label>
<select id="uploadFolderSelect" name="folder_id" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none">
<option value="">Root (No folder)</option>
for _, f := range folders {
<option value={ f.ID } selected?={ f.ID == activeFolder }>{ f.Name }</option>
}
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Link to Request Item <span class="text-gray-600">(optional)</span></label>
<select name="request_item_id" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none">
<option value="">None</option>
for _, req := range requests {
<option value={ req.ID }>{ req.ItemNumber } - { req.Description }</option>
}
</select>
</div>
<div class="flex justify-end gap-3">
<button type="button" onclick="document.getElementById('uploadModal').classList.add('hidden')" class="px-4 py-2 text-sm text-gray-400 border border-gray-700 rounded-lg hover:bg-gray-800 transition">Cancel</button>
<button type="submit" class="px-4 py-2 bg-teal-500 text-white text-sm font-medium rounded-lg hover:bg-teal-600 transition">Upload</button>
</div>
</form>
</div>
</div>
<!-- Upload Request List Modal (Section 3b) -->
<div id="uploadRequestListModal" class="hidden fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/60" onclick="document.getElementById('uploadRequestListModal').classList.add('hidden')"></div>
<div class="relative bg-gray-900 border border-gray-700 rounded-xl shadow-2xl w-full max-w-lg mx-4 p-6">
<div class="flex items-center justify-between mb-5">
<h2 class="text-lg font-bold">Upload Request / Diligence List</h2>
<button onclick="document.getElementById('uploadRequestListModal').classList.add('hidden')" class="text-gray-500 hover:text-gray-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
<form action="/deals/requests/upload" method="POST" enctype="multipart/form-data" class="space-y-4">
<input type="hidden" name="deal_id" value={ deal.ID }/>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Request List File <span class="text-gray-600">(Excel .xlsx or CSV — columns auto-detected)</span></label>
<div class="border-2 border-dashed border-gray-700 rounded-lg p-4 text-center hover:border-teal-500/50 transition cursor-pointer" onclick="document.getElementById('reqListFile').click()">
<svg class="w-6 h-6 text-gray-500 mx-auto mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path></svg>
<p class="text-sm text-gray-400">Click to upload Excel or CSV</p>
<p id="reqListFileName" class="text-xs text-teal-400 mt-1 hidden"></p>
</div>
<input type="file" id="reqListFile" name="request_list" accept=".csv,.xlsx,.xls" class="hidden" required onchange="if(this.files.length){var el=document.getElementById('reqListFileName');el.textContent=this.files[0].name;el.classList.remove('hidden')}"/>
</div>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Target Group</label>
<select name="target_group" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none">
<option value="all">All Groups</option>
<option value="Meridian Capital">Meridian Capital</option>
<option value="Summit Health Equity">Summit Health Equity</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Upload Mode</label>
<select name="upload_mode" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none">
<option value="add">Add to existing list</option>
<option value="replace">Replace existing list</option>
<option value="group_specific">Create group-specific list</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Folder Structure</label>
<select name="convert_folders" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none">
<option value="no">Keep current folder structure</option>
<option value="yes">Create folders from request sections</option>
</select>
</div>
<div class="flex justify-end gap-3 pt-2">
<button type="button" onclick="document.getElementById('uploadRequestListModal').classList.add('hidden')" class="px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-gray-200 border border-gray-700 hover:border-gray-600 transition">Cancel</button>
<button type="submit" class="px-4 py-2 rounded-lg bg-teal-500 text-white text-sm font-medium hover:bg-teal-600 transition">Upload & Process</button>
</div>
</form>
</div>
</div>
<!-- Add Statement Modal -->
<div id="addStatementModal" class="hidden fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/60" onclick="document.getElementById('addStatementModal').classList.add('hidden')"></div>
<div class="relative bg-gray-900 border border-gray-700 rounded-xl shadow-2xl w-full max-w-lg mx-4 p-6">
<div class="flex items-center justify-between mb-5">
<h2 class="text-lg font-bold">Add Statement</h2>
<button onclick="document.getElementById('addStatementModal').classList.add('hidden')" class="text-gray-500 hover:text-gray-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
<form action="/deals/responses/statement" method="POST" class="space-y-4">
<input type="hidden" name="deal_id" value={ deal.ID }/>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Title</label>
<input type="text" name="title" required placeholder="e.g. Revenue breakdown Q4 2025" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
</div>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Content (Markdown)</label>
<textarea name="body" rows="8" required placeholder="Type your response here..." class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none resize-none font-mono"></textarea>
</div>
<div class="flex justify-end gap-3 pt-2">
<button type="button" onclick="document.getElementById('addStatementModal').classList.add('hidden')" class="px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-gray-200 border border-gray-700 hover:border-gray-600 transition">Cancel</button>
<button type="submit" class="px-4 py-2 rounded-lg bg-teal-500 text-white text-sm font-medium hover:bg-teal-600 transition">Save Statement</button>
</div>
</form>
</div>
</div>
<!-- New Folder Modal -->
<div id="newFolderModal" class="hidden fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/60" onclick="document.getElementById('newFolderModal').classList.add('hidden')"></div>
<div class="relative bg-gray-900 border border-gray-700 rounded-xl shadow-2xl w-full max-w-sm mx-4 p-6">
<h2 class="text-lg font-bold mb-4">New Folder</h2>
<form action="/deals/folders/create" method="POST" class="space-y-4">
<input type="hidden" name="deal_id" value={ deal.ID }/>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Folder Name</label>
<input type="text" name="name" required class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
</div>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Parent Folder</label>
<select name="parent_id" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none">
<option value="">Root</option>
for _, f := range folders {
<option value={ f.ID }>{ f.Name }</option>
}
</select>
</div>
<div class="flex justify-end gap-3">
<button type="button" onclick="document.getElementById('newFolderModal').classList.add('hidden')" class="px-4 py-2 text-sm text-gray-400 border border-gray-700 rounded-lg hover:bg-gray-800 transition">Cancel</button>
<button type="submit" class="px-4 py-2 bg-teal-500 text-white text-sm font-medium rounded-lg hover:bg-teal-600 transition">Create</button>
</div>
</form>
</div>
</div>
<!-- Deal ID for JS -->
<input type="hidden" id="currentDealID" value={ deal.ID }/>
<!-- Folder Context Menu -->
<div id="folderContextMenu" class="hidden fixed z-50 bg-gray-800 border border-gray-700 rounded-lg shadow-xl py-1 min-w-[140px]">
<button onclick="reorderFolder('up')" class="block w-full text-left px-3 py-1.5 text-xs text-gray-300 hover:bg-gray-700 transition">Move Up</button>
<button onclick="reorderFolder('down')" class="block w-full text-left px-3 py-1.5 text-xs text-gray-300 hover:bg-gray-700 transition">Move Down</button>
<div class="border-t border-gray-700 my-1"></div>
<button onclick="renameFolder()" class="block w-full text-left px-3 py-1.5 text-xs text-gray-300 hover:bg-gray-700 transition">Rename</button>
<button onclick="deleteFolder()" class="block w-full text-left px-3 py-1.5 text-xs text-red-400 hover:bg-gray-700 transition">Delete</button>
</div>
<script>
// Tab switching
function showTab(name) {
document.getElementById('panel-documents').style.display = name === 'documents' ? '' : 'none';
document.getElementById('panel-requests').style.display = name === 'requests' ? '' : 'none';
document.getElementById('tab-documents').className = name === 'documents'
? 'px-4 py-2 text-sm font-medium border-b-2 border-teal-500 text-teal-400'
: 'px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-300';
document.getElementById('tab-requests').className = name === 'requests'
? 'px-4 py-2 text-sm font-medium border-b-2 border-teal-500 text-teal-400'
: 'px-4 py-2 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-300';
}
// Auto-switch tab based on ?tab= query param (e.g. after request list upload)
(function() {
var tab = new URLSearchParams(window.location.search).get('tab');
if (tab) { showTab(tab); }
})();
function filterFiles() {
var q = document.getElementById('fileSearch').value.toLowerCase();
document.querySelectorAll('.file-row').forEach(function(row) {
var name = row.getAttribute('data-filename').toLowerCase();
row.style.display = name.includes(q) ? '' : 'none';
});
}
// Status dropdown
function toggleStatusDropdown(event, dealID, fileID) {
event.stopPropagation();
document.querySelectorAll('[data-dropdown]').forEach(function(el) {
if (el.getAttribute('data-dropdown') !== fileID) el.classList.add('hidden');
});
var dd = document.querySelector('[data-dropdown="' + fileID + '"]');
dd.classList.toggle('hidden');
}
function updateFileStatus(dealID, fileID, status) {
fetch('/deals/' + dealID + '/files/' + fileID + '/status', {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({status: status})
}).then(function() { window.location.reload(); });
}
document.addEventListener('click', function() {
document.querySelectorAll('[data-dropdown]').forEach(function(el) { el.classList.add('hidden'); });
document.getElementById('folderContextMenu').classList.add('hidden');
});
// Upload modal: context-aware folder pre-selection
function openUploadModal() {
document.getElementById('uploadModal').classList.remove('hidden');
}
// Upload modal: drag-and-drop on the drop zone
var uploadDropZone = document.getElementById('uploadDropZone');
if (uploadDropZone) {
uploadDropZone.addEventListener('dragover', function(e) { e.preventDefault(); this.classList.add('border-teal-500'); });
uploadDropZone.addEventListener('dragleave', function() { this.classList.remove('border-teal-500'); });
uploadDropZone.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('border-teal-500');
if (e.dataTransfer.files.length > 0) {
document.getElementById('fileInput').files = e.dataTransfer.files;
var el = document.getElementById('uploadFileName');
el.textContent = e.dataTransfer.files[0].name;
el.classList.remove('hidden');
}
});
}
document.getElementById('fileInput').addEventListener('change', function() {
if (this.files.length > 0) {
var el = document.getElementById('uploadFileName');
el.textContent = this.files[0].name;
el.classList.remove('hidden');
}
});
// Drag-and-drop files directly onto the files panel
function handleFileDragOver(e) {
e.preventDefault();
document.getElementById('dropOverlay').classList.remove('hidden');
}
function handleFileDragLeave(e) {
if (!e.currentTarget.contains(e.relatedTarget)) {
document.getElementById('dropOverlay').classList.add('hidden');
}
}
function handleFileDrop(e) {
e.preventDefault();
document.getElementById('dropOverlay').classList.add('hidden');
if (e.dataTransfer.files.length > 0) {
var formData = new FormData();
formData.append('deal_id', document.getElementById('currentDealID').value);
formData.append('folder_id', new URLSearchParams(window.location.search).get('folder') || '');
for (var i = 0; i < e.dataTransfer.files.length; i++) {
formData.append('file', e.dataTransfer.files[i]);
}
fetch('/deals/files/upload', { method: 'POST', body: formData })
.then(function() { window.location.reload(); });
}
}
// Folder drag — supports reorder AND reparenting (make child / make root)
var draggedFolderID = null;
var draggedFolderParent = null;
var draggedDealID = null;
function handleFolderDragStart(e) {
// Stop bubbling so inner child drags don't fire parent's dragstart too
e.stopPropagation();
draggedFolderID = e.currentTarget.getAttribute('data-folder-id');
draggedFolderParent = e.currentTarget.getAttribute('data-folder-parent') || '';
draggedDealID = e.currentTarget.getAttribute('data-deal-id');
e.dataTransfer.effectAllowed = 'move';
e.currentTarget.style.opacity = '0.4';
// Show root zone only when a subfolder is being dragged
if (draggedFolderParent !== '') {
document.getElementById('rootDropZone').classList.remove('hidden');
}
}
function handleFolderDragEnd(e) {
e.currentTarget.style.opacity = '1';
document.getElementById('rootDropZone').classList.add('hidden');
// Clear all hover highlights
document.querySelectorAll('.folder-item').forEach(function(el) {
el.querySelector('a').style.outline = '';
});
}
function handleFolderDragOver(e) {
e.preventDefault();
e.stopPropagation();
var targetID = e.currentTarget.getAttribute('data-folder-id');
if (draggedFolderID && targetID !== draggedFolderID) {
e.dataTransfer.dropEffect = 'move';
// Visual: highlight the folder as a potential parent
e.currentTarget.querySelector('a').style.outline = '2px solid rgba(20,184,166,0.5)';
}
}
function handleFolderDragLeave(e) {
e.stopPropagation();
if (e.currentTarget.querySelector('a')) {
e.currentTarget.querySelector('a').style.outline = '';
}
}
function handleFolderDrop(e) {
e.preventDefault();
e.stopPropagation();
var targetID = e.currentTarget.getAttribute('data-folder-id');
var dealID = e.currentTarget.getAttribute('data-deal-id');
if (e.currentTarget.querySelector('a')) {
e.currentTarget.querySelector('a').style.outline = '';
}
document.getElementById('rootDropZone').classList.add('hidden');
if (!draggedFolderID || !targetID || draggedFolderID === targetID) {
draggedFolderID = null;
return;
}
// Determine drop intent: top 40% of element → reorder (same level),
// bottom 60% → make the dragged folder a child of the target
var rect = e.currentTarget.getBoundingClientRect();
var relY = e.clientY - rect.top;
var ratio = relY / rect.height;
if (ratio < 0.4) {
// Reorder at same level (existing behaviour)
var form = new FormData();
form.append('folder_id', draggedFolderID);
form.append('deal_id', dealID);
form.append('direction', 'swap');
form.append('target_id', targetID);
fetch('/deals/folders/reorder', { method: 'POST', body: form })
.then(function() { window.location.reload(); });
} else {
// Reparent: make dragged folder a child of target
var form = new FormData();
form.append('folder_id', draggedFolderID);
form.append('new_parent_id', targetID);
form.append('deal_id', dealID);
fetch('/deals/folders/reparent', { method: 'POST', body: form })
.then(function() { window.location.reload(); });
}
draggedFolderID = null;
}
// Root drop zone — promotes a subfolder to root level
function handleRootZoneDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
document.getElementById('rootDropZone').classList.add('border-teal-400/80', 'bg-teal-500/5', 'text-teal-300');
}
function handleRootZoneDragLeave(e) {
document.getElementById('rootDropZone').classList.remove('border-teal-400/80', 'bg-teal-500/5', 'text-teal-300');
}
function handleRootZoneDrop(e) {
e.preventDefault();
document.getElementById('rootDropZone').classList.add('hidden');
document.getElementById('rootDropZone').classList.remove('border-teal-400/80', 'bg-teal-500/5', 'text-teal-300');
if (!draggedFolderID || !draggedDealID) return;
var form = new FormData();
form.append('folder_id', draggedFolderID);
form.append('new_parent_id', ''); // root
form.append('deal_id', draggedDealID);
fetch('/deals/folders/reparent', { method: 'POST', body: form })
.then(function() { window.location.reload(); });
draggedFolderID = null;
}
// Folder context menu (right-click)
var ctxFolderID = null, ctxDealID = null;
function showFolderContextMenu(e, folderID, dealID) {
e.preventDefault();
ctxFolderID = folderID;
ctxDealID = dealID;
var menu = document.getElementById('folderContextMenu');
menu.style.left = e.pageX + 'px';
menu.style.top = e.pageY + 'px';
menu.classList.remove('hidden');
}
function reorderFolder(direction) {
var form = new FormData();
form.append('folder_id', ctxFolderID);
form.append('deal_id', ctxDealID);
form.append('direction', direction);
fetch('/deals/folders/reorder', { method: 'POST', body: form })
.then(function() { window.location.reload(); });
}
function renameFolder() {
var name = prompt('New folder name:');
if (name) {
var form = new FormData();
form.append('folder_id', ctxFolderID);
form.append('deal_id', ctxDealID);
form.append('name', name);
fetch('/deals/folders/rename', { method: 'POST', body: form })
.then(function() { window.location.reload(); });
}
}
function deleteFolder() {
if (confirm('Delete this folder?')) {
var form = new FormData();
form.append('folder_id', ctxFolderID);
form.append('deal_id', ctxDealID);
fetch('/deals/folders/delete', { method: 'POST', body: form })
.then(function() { window.location.reload(); });
}
}
// ---- Responses / AI matching / Assignment rules ----
var dealID = document.getElementById('currentDealID').value;
// Load pending AI matches
function loadPendingMatches() {
fetch('/deals/responses/pending/' + dealID)
.then(function(r) { return r.json(); })
.then(function(links) {
var panel = document.getElementById('pendingMatchesPanel');
var list = document.getElementById('pendingMatchesList');
if (!links || links.length === 0) {
panel.classList.add('hidden');
return;
}
panel.classList.remove('hidden');
panel.querySelector('h3 span').textContent = links.length + ' AI-suggested match' + (links.length > 1 ? 'es' : '') + ' waiting for review';
var html = '';
links.forEach(function(l) {
var pct = Math.round(l.confidence * 100);
html += '<div class="flex items-center gap-3 p-2 bg-gray-800/50 rounded-lg text-sm" id="match-' + l.request_id + '-' + l.chunk_id + '">' +
'<div class="flex-1 text-gray-300 truncate">' + escHtml(l.request_desc) + '</div>' +
'<span class="text-gray-600 text-xs">&#8594;</span>' +
'<div class="flex-1 text-teal-400 truncate">' + escHtml(l.response_title) + '</div>' +
'<span class="text-xs text-gray-500">' + pct + '%</span>' +
'<button onclick="confirmLink(\'' + l.request_id + '\',\'' + l.response_id + '\',\'' + l.chunk_id + '\')" class="px-2 py-1 text-xs bg-green-600/20 text-green-400 rounded hover:bg-green-600/30 transition">Confirm</button>' +
'<button onclick="rejectLink(\'' + l.request_id + '\',\'' + l.response_id + '\',\'' + l.chunk_id + '\')" class="px-2 py-1 text-xs bg-red-600/20 text-red-400 rounded hover:bg-red-600/30 transition">Reject</button>' +
'</div>';
});
list.innerHTML = html;
});
}
function confirmLink(reqID, respID, chunkID) {
var form = new FormData();
form.append('request_id', reqID);
form.append('response_id', respID);
form.append('chunk_id', chunkID);
var el = document.getElementById('match-' + reqID + '-' + chunkID);
fetch('/deals/responses/confirm', { method: 'POST', body: form })
.then(function() { if (el) el.innerHTML = '<span class="text-xs text-green-400">Confirmed</span>'; });
}
function rejectLink(reqID, respID, chunkID) {
var form = new FormData();
form.append('request_id', reqID);
form.append('response_id', respID);
form.append('chunk_id', chunkID);
var el = document.getElementById('match-' + reqID + '-' + chunkID);
fetch('/deals/responses/reject', { method: 'POST', body: form })
.then(function() { if (el) el.remove(); });
}
function escHtml(s) {
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
// Load pending matches on page load
loadPendingMatches();
// Assignment rules management
var currentRules = [];
function loadAssignmentRules() {
fetch('/deals/assignment-rules/' + dealID)
.then(function(r) { return r.json(); })
.then(function(rules) {
currentRules = rules;
renderRules();
});
}
function renderRules() {
var body = document.getElementById('rulesTableBody');
if (currentRules.length === 0) {
body.innerHTML = '<div class="text-xs text-gray-500 italic">No rules configured.</div>';
return;
}
var html = '';
currentRules.forEach(function(r, i) {
html += '<div class="flex items-center gap-2">' +
'<span class="text-sm text-gray-300 flex-1">' + escHtml(r.keyword) + '</span>' +
'<span class="text-sm text-gray-400 flex-1">' + escHtml(r.assignee_name || r.assignee_id) + '</span>' +
'<button onclick="removeRule(' + i + ')" class="text-xs text-red-400 hover:text-red-300">Remove</button>' +
'</div>';
});
body.innerHTML = html;
}
function addAssignmentRule() {
var keyword = document.getElementById('newRuleKeyword').value.trim();
var assignee = document.getElementById('newRuleAssignee').value.trim();
if (!keyword || !assignee) return;
currentRules.push({ keyword: keyword, assignee_id: assignee, assignee_name: assignee });
document.getElementById('newRuleKeyword').value = '';
document.getElementById('newRuleAssignee').value = '';
renderRules();
}
function removeRule(index) {
currentRules.splice(index, 1);
renderRules();
}
function saveAssignmentRules() {
var form = new FormData();
form.append('deal_id', dealID);
form.append('rules', JSON.stringify(currentRules.map(function(r) { return { keyword: r.keyword, assignee_id: r.assignee_id }; })));
fetch('/deals/assignment-rules/save', { method: 'POST', body: form })
.then(function() { window.location.reload(); });
}
// Load rules on page load
loadAssignmentRules();
// Poll extraction status for files (5s interval, stop when all done)
(function pollExtraction() {
var pendingCells = document.querySelectorAll('.extraction-status[data-status="pending"], .extraction-status[data-status="processing"]');
if (pendingCells.length === 0) return;
setTimeout(function() {
window.location.reload();
}, 5000);
})();
</script>
</div>
}
}
templ requestStatusPill(status string, confirmedLinks int) {
if confirmedLinks > 0 {
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-500/10 text-green-400">&#10003; Answered</span>
} else if status == "in_progress" {
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-500/10 text-blue-400">In Progress</span>
} else if status == "answered" {
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-500/10 text-green-400">&#10003; Answered</span>
} else if status == "not_applicable" {
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-500/10 text-gray-500">N/A</span>
} else {
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-500/10 text-gray-400">Open</span>
}
}
templ statusOption(dealID string, fileID string, status string, label string) {
<button onclick={ templ.ComponentScript{Call: fmt.Sprintf("updateFileStatus('%s', '%s', '%s')", dealID, fileID, status)} } class="block w-full text-left px-3 py-1.5 text-xs text-gray-300 hover:bg-gray-700 transition">
{ label }
</button>
}
func formatFileSize(bytes int64) string {
if bytes < 1024 {
return fmt.Sprintf("%d B", bytes)
}
if bytes < 1024*1024 {
return fmt.Sprintf("%d KB", bytes/1024)
}
return fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024))
}
func formatFileDate(t time.Time) string {
if t.IsZero() {
return ""
}
now := time.Now()
if t.Year() == now.Year() {
return t.Format("Jan 2")
}
return t.Format("Jan 2, 2006")
}
templ fileIcon(name string) {
<div class={ "w-7 h-7 rounded flex items-center justify-center text-xs font-semibold text-white",
templ.KV("bg-red-500", hasSuffix(name, ".pdf")),
templ.KV("bg-green-600", hasSuffix(name, ".xlsx") || hasSuffix(name, ".csv")),
templ.KV("bg-blue-500", hasSuffix(name, ".doc") || hasSuffix(name, ".docx")),
templ.KV("bg-gray-600", !hasSuffix(name, ".pdf") && !hasSuffix(name, ".xlsx") && !hasSuffix(name, ".csv") && !hasSuffix(name, ".doc") && !hasSuffix(name, ".docx")) }>
if hasSuffix(name, ".pdf") {
PDF
} else if hasSuffix(name, ".xlsx") || hasSuffix(name, ".csv") {
XLS
} else if hasSuffix(name, ".doc") || hasSuffix(name, ".docx") {
DOC
} else {
FILE
}
</div>
}
func splitLinkedFiles(ids string) []string {
if ids == "" {
return nil
}
parts := strings.Split(ids, ",")
var result []string
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
result = append(result, p)
}
}
return result
}
func getLinkedFileName(files []*model.File, fileID string) string {
for _, f := range files {
if f.ID == fileID {
return f.Name
}
}
return fileID
}
func hasSuffix(s, suffix string) bool {
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}