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 := ` + + + + + + ` + + 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(` + + + + + + `, bg, downloads, lastAccess, timeStr, views) + } + if !hasRows { + html += `` + } + + html += `
Buyer GroupDownloadsLast AccessedTotal TimeFiles Viewed
%s%d%s%s%d
No buyer activity recorded yet.
` + 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