feat: analytics per-buyer stats with buyer activity table
This commit is contained in:
parent
cdca770e45
commit
c222b5a3c3
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue