dealroom/templates/dashboard.templ

246 lines
12 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>
<form action="/deals/create" method="POST" class="space-y-4">
<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>
<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 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-2 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>
<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 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>
</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 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>
}