337 lines
16 KiB
Plaintext
337 lines
16 KiB
Plaintext
package templates
|
|
|
|
import "dealroom/internal/model"
|
|
import "fmt"
|
|
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)
|
|
</div>
|
|
<p class="text-sm text-gray-500 mt-1">{ deal.TargetCompany } · { deal.Description }</p>
|
|
</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">—</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">—</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>
|
|
for _, folder := range folders {
|
|
if folder.ParentID == "" {
|
|
<div class="mb-1">
|
|
<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", 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-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 -->
|
|
for _, child := range folders {
|
|
if child.ParentID == folder.ID {
|
|
<div class="ml-4">
|
|
<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", 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-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>
|
|
}
|
|
}
|
|
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 class="col-span-3 bg-gray-900 rounded-lg border border-gray-800">
|
|
<!-- 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"/>
|
|
if profile.Role != "buyer" {
|
|
<button 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>
|
|
<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-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>
|
|
</tr>
|
|
}
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Requests Tab -->
|
|
<div id="panel-requests" style="display:none">
|
|
<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-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">Atlas Notes</th>
|
|
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider w-16">Conf.</th>
|
|
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider">Buyer</th>
|
|
<th class="text-left px-4 py-2.5 text-xs font-medium text-gray-500 uppercase tracking-wider">Seller</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">@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">
|
|
if req.AtlasStatus == "Complete" {
|
|
<span class="text-teal-400/80">Found in Q3 Financials.pdf</span>
|
|
} else {
|
|
<span class="text-gray-600 italic">No notes</span>
|
|
}
|
|
</td>
|
|
<td class="px-4 py-2.5 text-xs text-gray-500">
|
|
if req.Confidence > 0 {
|
|
{ fmt.Sprintf("%d%%", req.Confidence) }
|
|
}
|
|
</td>
|
|
<td class="px-4 py-2.5 text-xs text-gray-400 max-w-[140px] truncate">{ req.BuyerComment }</td>
|
|
<td class="px-4 py-2.5 text-xs text-gray-400 max-w-[140px] truncate">{ req.SellerComment }</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</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>
|
|
|
|
<script>
|
|
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';
|
|
}
|
|
|
|
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';
|
|
});
|
|
}
|
|
|
|
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');
|
|
});
|
|
});
|
|
</script>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
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 hasSuffix(s, suffix string) bool {
|
|
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
|
|
}
|