dealroom/templates/dealroom.templ

309 lines
15 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">&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>
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 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>
<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
}