167 lines
6.9 KiB
Plaintext
167 lines
6.9 KiB
Plaintext
package templates
|
|
|
|
import "dealroom/internal/model"
|
|
import "fmt"
|
|
|
|
templ Dashboard(profile *model.Profile, deals []*model.Deal, activities []*model.DealActivity, fileCounts map[string]int) {
|
|
@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>
|
|
<a href="/deals" 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
|
|
</a>
|
|
</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("DOCUMENTS", fmt.Sprintf("%d", totalFiles(fileCounts)), "across all rooms", "file")
|
|
@statCard("ACTIVE DEALS", fmt.Sprintf("%d", countActive(deals)), fmt.Sprintf("%d in diligence", countByStage(deals, "due_diligence")), "users")
|
|
@statCard("AVG. CLOSE PROB.", fmt.Sprintf("%d%%", avgProbability(deals)), "across portfolio", "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">{ fmt.Sprintf("%d%%", deal.CloseProbability) }</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") }</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</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 totalFiles(fc map[string]int) int {
|
|
total := 0
|
|
for _, c := range fc {
|
|
total += c
|
|
}
|
|
return total
|
|
}
|
|
|
|
func avgProbability(deals []*model.Deal) int {
|
|
if len(deals) == 0 {
|
|
return 0
|
|
}
|
|
sum := 0
|
|
for _, d := range deals {
|
|
sum += d.CloseProbability
|
|
}
|
|
return sum / len(deals)
|
|
}
|
|
|
|
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 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>
|
|
}
|