feat: analytics per-buyer stats with buyer activity table

This commit is contained in:
James 2026-02-23 02:59:48 -05:00
parent cdca770e45
commit c222b5a3c3
5 changed files with 81 additions and 3 deletions

View File

@ -247,6 +247,9 @@ var additiveMigrationStmts = []string{
// Section 9: buyer-specific requests
`ALTER TABLE diligence_requests ADD COLUMN is_buyer_specific INTEGER DEFAULT 0`,
`ALTER TABLE diligence_requests ADD COLUMN visible_to_buyer_group TEXT DEFAULT ''`,
// Section 13: analytics per-buyer
`ALTER TABLE deal_activity ADD COLUMN buyer_group TEXT DEFAULT ''`,
`ALTER TABLE deal_activity ADD COLUMN time_spent_seconds INTEGER DEFAULT 0`,
}
func seed(db *sql.DB) error {

View File

@ -1,6 +1,7 @@
package handler
import (
"fmt"
"net/http"
"dealroom/templates"
@ -40,3 +41,60 @@ func (h *Handler) handleAnalytics(w http.ResponseWriter, r *http.Request) {
templates.AnalyticsPage(profile, stats, deals, dealID).Render(r.Context(), w)
}
func (h *Handler) handleAnalyticsBuyers(w http.ResponseWriter, r *http.Request) {
dealID := r.URL.Query().Get("deal_id")
if dealID == "" {
w.Write([]byte(`<p class="text-sm text-gray-500">Select a deal to see buyer activity.</p>`))
return
}
rows, err := h.db.Query(`
SELECT COALESCE(buyer_group, 'Unknown') as bg,
COUNT(CASE WHEN activity_type = 'download' THEN 1 END) as downloads,
MAX(created_at) as last_access,
SUM(COALESCE(time_spent_seconds, 0)) as total_time,
COUNT(CASE WHEN activity_type = 'view' THEN 1 END) as views
FROM deal_activity WHERE deal_id = ? AND COALESCE(buyer_group, '') != ''
GROUP BY COALESCE(buyer_group, 'Unknown')
ORDER BY last_access DESC`, dealID)
if err != nil {
http.Error(w, "Error", 500)
return
}
defer rows.Close()
w.Header().Set("Content-Type", "text/html")
html := `<table class="w-full"><thead><tr class="border-b border-gray-800">
<th class="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase">Buyer Group</th>
<th class="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase">Downloads</th>
<th class="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase">Last Accessed</th>
<th class="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase">Total Time</th>
<th class="text-left px-4 py-2 text-xs font-medium text-gray-500 uppercase">Files Viewed</th>
</tr></thead><tbody class="divide-y divide-gray-800/50">`
hasRows := false
for rows.Next() {
hasRows = true
var bg, lastAccess string
var downloads, totalTime, views int
rows.Scan(&bg, &downloads, &lastAccess, &totalTime, &views)
timeStr := fmt.Sprintf("%dm", totalTime/60)
if totalTime < 60 {
timeStr = fmt.Sprintf("%ds", totalTime)
}
html += fmt.Sprintf(`<tr class="hover:bg-gray-800/30">
<td class="px-4 py-2 text-sm font-medium text-teal-400">%s</td>
<td class="px-4 py-2 text-sm text-gray-400">%d</td>
<td class="px-4 py-2 text-xs text-gray-500">%s</td>
<td class="px-4 py-2 text-sm text-gray-400">%s</td>
<td class="px-4 py-2 text-sm text-gray-400">%d</td>
</tr>`, bg, downloads, lastAccess, timeStr, views)
}
if !hasRows {
html += `<tr><td colspan="5" class="px-4 py-4 text-center text-gray-600 text-sm">No buyer activity recorded yet.</td></tr>`
}
html += `</tbody></table>`
w.Write([]byte(html))
}

View File

@ -88,6 +88,7 @@ mux.HandleFunc("/auth/logout", h.handleLogout)
mux.HandleFunc("/deals/files/download/", h.requireAuth(h.handleFileDownload))
mux.HandleFunc("/deals/files/comments/", h.requireAuth(h.handleFileComments))
mux.HandleFunc("/deals/search/", h.requireAuth(h.handleDealSearch))
mux.HandleFunc("/analytics/buyers", h.requireAuth(h.handleAnalyticsBuyers))
// HTMX partials
mux.HandleFunc("/htmx/request-comment", h.requireAuth(h.handleUpdateComment))

View File

@ -130,8 +130,10 @@ type DealActivity struct {
ResourceType string
ResourceName string
ResourceID string
Details string
CreatedAt time.Time
Details string
BuyerGroup string
TimeSpentSeconds int
CreatedAt time.Time
// Computed
UserName string
DealName string

View File

@ -10,6 +10,10 @@ type AnalyticsStats struct {
CompletionPct int
}
func completionWidth(pct int) templ.Attributes {
return templ.Attributes{"style": fmt.Sprintf("width: %d%%", pct)}
}
templ AnalyticsPage(profile *model.Profile, stats *AnalyticsStats, deals []*model.Deal, selectedDealID string) {
@Layout(profile, "analytics") {
<div class="space-y-5">
@ -45,11 +49,21 @@ templ AnalyticsPage(profile *model.Profile, stats *AnalyticsStats, deals []*mode
<div class="text-xs text-gray-500 uppercase tracking-wider mb-2">Request Completion</div>
<div class="text-3xl font-bold text-green-400">{ fmt.Sprintf("%d%%", stats.CompletionPct) }</div>
<div class="mt-3 w-full bg-gray-800 rounded-full h-2">
<div class="bg-green-500 h-2 rounded-full" style="width: 50%"> </div>
<div class="bg-green-500 h-2 rounded-full" { completionWidth(stats.CompletionPct)... }> </div>
</div>
</div>
</div>
<!-- Buyer Activity -->
if selectedDealID != "" {
<div class="bg-gray-900 rounded-lg border border-gray-800 p-6">
<h2 class="text-sm font-semibold mb-4">Buyer Activity</h2>
<div hx-get={ fmt.Sprintf("/analytics/buyers?deal_id=%s", selectedDealID) } hx-trigger="load" hx-swap="innerHTML">
<p class="text-sm text-gray-500">Loading buyer activity...</p>
</div>
</div>
}
<!-- Completion by Status -->
<div class="bg-gray-900 rounded-lg border border-gray-800 p-6">
<h2 class="text-sm font-semibold mb-4">Request Status Breakdown</h2>