268 lines
13 KiB
Plaintext
268 lines
13 KiB
Plaintext
package templates
|
|
|
|
import "dealroom/internal/model"
|
|
import "fmt"
|
|
import "time"
|
|
|
|
templ Dashboard(profile *model.Profile, deals []*model.Deal, activities []*model.DealActivity, fileCounts map[string]int, lastActivity map[string]*time.Time) {
|
|
@Layout(profile, "dashboard") {
|
|
<div class="space-y-6">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-2xl font-bold">Dashboard</h1>
|
|
<p class="text-sm text-gray-500 mt-1">Overview of all active deal rooms and recent activity.</p>
|
|
</div>
|
|
<button onclick="document.getElementById('newRoomModal').classList.remove('hidden')" class="h-9 px-4 rounded-lg bg-teal-500 text-white text-sm font-medium flex items-center gap-1.5 hover:bg-teal-600 transition">
|
|
<svg class="w-3.5 h-3.5" 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 Room
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="grid grid-cols-4 gap-4">
|
|
@statCard("ACTIVE ROOMS", fmt.Sprintf("%d", countActive(deals)), fmt.Sprintf("%d total", len(deals)), "folder")
|
|
@statCard("PRE-MARKETING", fmt.Sprintf("%d", countByStage(deals, "pipeline")), "in pipeline", "file")
|
|
@statCard("IOI STAGE", fmt.Sprintf("%d", countIOIStage(deals)), "initial review / LOI", "users")
|
|
@statCard("CLOSED", fmt.Sprintf("%d", countByStage(deals, "closed")), "deals closed", "trend")
|
|
</div>
|
|
|
|
<!-- Content Grid -->
|
|
<div class="grid grid-cols-3 gap-6">
|
|
<!-- Deal Rooms -->
|
|
<div class="col-span-2 bg-gray-900 rounded-lg border border-gray-800">
|
|
<div class="flex items-center justify-between p-4 border-b border-gray-800">
|
|
<h2 class="text-sm font-semibold">Active Deal Rooms</h2>
|
|
<a href="/deals" class="text-xs text-teal-400 hover:underline flex items-center gap-1">
|
|
View all
|
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 17L17 7M17 7H7m10 0v10"></path></svg>
|
|
</a>
|
|
</div>
|
|
if len(deals) == 0 {
|
|
<div class="p-8 text-center text-gray-500 text-sm">
|
|
No deal rooms yet. Create one to get started.
|
|
</div>
|
|
} else {
|
|
<div class="divide-y divide-gray-800">
|
|
for _, deal := range deals {
|
|
<a href={ templ.SafeURL(fmt.Sprintf("/deals/%s", deal.ID)) } class="flex items-center gap-4 px-4 py-3 hover:bg-gray-800/50 transition group">
|
|
<div class="w-8 h-8 rounded bg-teal-500/10 flex items-center justify-center text-teal-400 text-xs font-bold shrink-0">
|
|
{ string(deal.Name[len(deal.Name)-1:]) }
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm font-medium group-hover:text-teal-400 transition">{ deal.Name }</span>
|
|
@StageBadge(deal.Stage)
|
|
</div>
|
|
<p class="text-xs text-gray-500 mt-0.5">{ fmt.Sprintf("%d docs", fileCounts[deal.ID]) } · { deal.TargetCompany }</p>
|
|
</div>
|
|
<div class="text-xs text-gray-500">{ formatLastAccessed(lastActivity[deal.ID]) }</div>
|
|
</a>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Recent Activity -->
|
|
<div class="bg-gray-900 rounded-lg border border-gray-800">
|
|
<div class="flex items-center justify-between p-4 border-b border-gray-800">
|
|
<h2 class="text-sm font-semibold">Recent Activity</h2>
|
|
<a href="/audit" class="text-xs text-teal-400 hover:underline flex items-center gap-1">
|
|
Full log
|
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 17L17 7M17 7H7m10 0v10"></path></svg>
|
|
</a>
|
|
</div>
|
|
<div class="p-4 space-y-4">
|
|
for _, act := range activities {
|
|
<div class="flex items-start gap-3">
|
|
@activityIcon(act.ActivityType)
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm">
|
|
<span class="font-medium">{ act.ActivityType }</span>
|
|
<span class="text-gray-400"> { act.ResourceName }</span>
|
|
</p>
|
|
<p class="text-xs text-gray-500 mt-0.5">
|
|
{ act.UserName } · { act.CreatedAt.Format("Jan 2, 3:04 PM") }
|
|
if act.DealName != "" {
|
|
<span class="text-gray-600"> · </span>
|
|
<a href={ templ.SafeURL(fmt.Sprintf("/deals/%s", act.DealID)) } class="text-teal-400 hover:underline">{ act.DealName }</a>
|
|
}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- New Room Modal -->
|
|
<div id="newRoomModal" class="hidden fixed inset-0 z-50 flex items-center justify-center">
|
|
<div class="absolute inset-0 bg-black/60" onclick="document.getElementById('newRoomModal').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">Create New Room</h2>
|
|
<button onclick="document.getElementById('newRoomModal').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>
|
|
@newRoomForm()
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
func countActive(deals []*model.Deal) int {
|
|
count := 0
|
|
for _, d := range deals {
|
|
if d.Stage != "closed" && d.Stage != "dead" {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func countByStage(deals []*model.Deal, stage string) int {
|
|
count := 0
|
|
for _, d := range deals {
|
|
if d.Stage == stage {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func countIOIStage(deals []*model.Deal) int {
|
|
count := 0
|
|
for _, d := range deals {
|
|
if d.Stage == "loi" || d.Stage == "initial_review" {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func totalFiles(fc map[string]int) int {
|
|
total := 0
|
|
for _, c := range fc {
|
|
total += c
|
|
}
|
|
return total
|
|
}
|
|
|
|
func formatLastAccessed(t *time.Time) string {
|
|
if t == nil {
|
|
return "Never accessed"
|
|
}
|
|
return "Last accessed " + t.Format("Jan 2")
|
|
}
|
|
|
|
templ statCard(label, value, subtitle, iconType string) {
|
|
<div class="bg-gray-900 rounded-lg border border-gray-800 p-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<span class="text-xs font-medium text-gray-500 uppercase tracking-wider">{ label }</span>
|
|
<div class="text-teal-400">
|
|
if iconType == "folder" {
|
|
<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="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>
|
|
}
|
|
if iconType == "file" {
|
|
<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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
|
|
}
|
|
if iconType == "users" {
|
|
<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 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
|
|
}
|
|
if iconType == "trend" {
|
|
<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="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="text-2xl font-bold">{ value }</div>
|
|
<p class="text-xs text-gray-500 mt-1">{ subtitle }</p>
|
|
</div>
|
|
}
|
|
|
|
templ newRoomForm() {
|
|
<form action="/deals/create" method="POST" class="space-y-4 max-h-[70vh] overflow-y-auto pr-1">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-400 mb-1">Project Name <span class="text-red-400">*</span></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">Company Name</label>
|
|
<input type="text" name="target_company" 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="pipeline">Pipeline</option>
|
|
<option value="loi">LOI Stage</option>
|
|
<option value="initial_review">Initial Review</option>
|
|
<option value="due_diligence">Due Diligence</option>
|
|
<option value="final_negotiation">Final Negotiation</option>
|
|
<option value="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" placeholder="e.g. Healthcare, Fintech" 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" 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">USD</option>
|
|
<option value="EUR">EUR</option>
|
|
<option value="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" 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" 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" 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"></textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-400 mb-1">Folder Structure <span class="text-gray-600">(one path per line, e.g. Financial/Q4 Reports)</span></label>
|
|
<textarea name="folder_structure" rows="3" placeholder="Financial Documents Legal Documents Technical DD/Architecture Technical DD/Security" 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>
|
|
<label class="block text-xs font-medium text-gray-400 mb-1">Initial Team <span class="text-gray-600">(one email per line)</span></label>
|
|
<textarea name="invite_emails" rows="2" placeholder="analyst@company.com associate@company.com" 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('newRoomModal').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">Create Room</button>
|
|
</div>
|
|
</form>
|
|
}
|
|
|
|
templ activityIcon(actType string) {
|
|
<div class={ "w-6 h-6 rounded-full flex items-center justify-center shrink-0",
|
|
templ.KV("bg-teal-500/20 text-teal-400", actType == "upload"),
|
|
templ.KV("bg-blue-500/20 text-blue-400", actType == "view"),
|
|
templ.KV("bg-amber-500/20 text-amber-400", actType == "edit"),
|
|
templ.KV("bg-purple-500/20 text-purple-400", actType == "download"),
|
|
templ.KV("bg-gray-500/20 text-gray-400", actType != "upload" && actType != "view" && actType != "edit" && actType != "download") }>
|
|
<div class="w-2 h-2 rounded-full bg-current"></div>
|
|
</div>
|
|
}
|