diff --git a/internal/db/migrate.go b/internal/db/migrate.go
index f1fe5a0..913f243 100644
--- a/internal/db/migrate.go
+++ b/internal/db/migrate.go
@@ -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 {
diff --git a/internal/handler/analytics.go b/internal/handler/analytics.go
index fe285ce..ab319ea 100644
--- a/internal/handler/analytics.go
+++ b/internal/handler/analytics.go
@@ -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(`
Select a deal to see buyer activity.
`))
+ 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 := `
+ | Buyer Group |
+ Downloads |
+ Last Accessed |
+ Total Time |
+ Files Viewed |
+
`
+
+ 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(`
+ | %s |
+ %d |
+ %s |
+ %s |
+ %d |
+
`, bg, downloads, lastAccess, timeStr, views)
+ }
+ if !hasRows {
+ html += `| No buyer activity recorded yet. |
`
+ }
+
+ html += `
`
+ w.Write([]byte(html))
+}
diff --git a/internal/handler/handler.go b/internal/handler/handler.go
index 7515939..c05e433 100644
--- a/internal/handler/handler.go
+++ b/internal/handler/handler.go
@@ -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))
diff --git a/internal/model/models.go b/internal/model/models.go
index 179611e..a06db6c 100644
--- a/internal/model/models.go
+++ b/internal/model/models.go
@@ -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
diff --git a/templates/analytics.templ b/templates/analytics.templ
index ef18d30..df9c36a 100644
--- a/templates/analytics.templ
+++ b/templates/analytics.templ
@@ -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") {
@@ -45,11 +49,21 @@ templ AnalyticsPage(profile *model.Profile, stats *AnalyticsStats, deals []*mode
Request Completion
{ fmt.Sprintf("%d%%", stats.CompletionPct) }
+
+ if selectedDealID != "" {
+
+
Buyer Activity
+
+
Loading buyer activity...
+
+
+ }
+
Request Status Breakdown