package httpx import ( "encoding/csv" "net/http" "strconv" "time" ) func (s *Server) routesMetrics(mux *http.ServeMux) { mux.Handle("GET /api/v1/developer/metrics", s.requireAuth(s.requireRole("developer", http.HandlerFunc(s.handleDeveloperMetrics)))) mux.Handle("GET /api/v1/consultant/metrics", s.requireAuth(s.requireRole("consultant", http.HandlerFunc(s.handleConsultantMetrics)))) mux.Handle("GET /api/v1/leaderboard", s.requireAuth(http.HandlerFunc(s.handleLeaderboard))) } func metricsWindow(r *http.Request) (time.Time, time.Time) { parse := func(s string, def time.Time) time.Time { if t, err := time.Parse("2006-01-02", s); err == nil { return t } return def } now := time.Now().UTC() from := parse(r.URL.Query().Get("from"), now.AddDate(0, -3, 0)) to := parse(r.URL.Query().Get("to"), now).Add(24*time.Hour - time.Nanosecond) return from, to } func (s *Server) nameOf(r *http.Request, id string) string { if u, err := s.store.UserByID(r.Context(), id); err == nil { return u.Name } if c, err := s.store.CustomerByID(r.Context(), id); err == nil { return c.Name } return id } func (s *Server) handleDeveloperMetrics(w http.ResponseWriter, r *http.Request) { me := CurrentUser(r.Context()).ID from, to := metricsWindow(r) if r.URL.Query().Get("format") == "csv" { s.writeAwardsCSV(w, r, me, "", "", from, to) return } total, tasks, err := s.store.AwardTotals(r.Context(), me, "", "", from, to) if err != nil { s.internalError(w, r, "award totals", err) return } weekly, err := s.store.WeeklyAwards(r.Context(), me, "", "", from, to) if err != nil { s.internalError(w, r, "weekly", err) return } perCustomer, err := s.store.AwardsGroupedBy(r.Context(), "customerId", me, "", "", from, to) if err != nil { s.internalError(w, r, "per customer", err) return } for i := range perCustomer { perCustomer[i].Key = s.nameOf(r, perCustomer[i].Key) } approved, changes, err := s.store.ReviewOutcomes(r.Context(), me, from, to) if err != nil { s.internalError(w, r, "outcomes", err) return } rate := 0.0 if approved+changes > 0 { rate = float64(approved) / float64(approved+changes) } minutes, err := s.store.TimeLogged(r.Context(), me, from, to) if err != nil { s.internalError(w, r, "time logged", err) return } avgHours, err := s.store.AvgAssignToApproveHours(r.Context(), me, from, to) if err != nil { s.internalError(w, r, "lead time", err) return } writeJSON(w, http.StatusOK, map[string]any{ "totalBounty": total, "tasksCompleted": tasks, "approvalRate": rate, "approved": approved, "changesRequested": changes, "timeLoggedMinutes": minutes, "avgAssignToApproveHours": avgHours, "weekly": weekly, "perCustomer": perCustomer, "from": from, "to": to, }) } func (s *Server) handleConsultantMetrics(w http.ResponseWriter, r *http.Request) { me := CurrentUser(r.Context()) from, to := metricsWindow(r) consultantID := me.ID if me.Roles.Admin { consultantID = "" // global view (§3) } customerID := r.URL.Query().Get("customerId") developerID := r.URL.Query().Get("developerId") if r.URL.Query().Get("format") == "csv" { s.writeAwardsCSV(w, r, developerID, consultantID, customerID, from, to) return } total, tasks, err := s.store.AwardTotals(r.Context(), developerID, consultantID, customerID, from, to) if err != nil { s.internalError(w, r, "totals", err) return } weekly, err := s.store.WeeklyAwards(r.Context(), developerID, consultantID, customerID, from, to) if err != nil { s.internalError(w, r, "weekly", err) return } perDeveloper, err := s.store.AwardsGroupedBy(r.Context(), "developerId", developerID, consultantID, customerID, from, to) if err != nil { s.internalError(w, r, "per developer", err) return } perCustomer, err := s.store.AwardsGroupedBy(r.Context(), "customerId", developerID, consultantID, customerID, from, to) if err != nil { s.internalError(w, r, "per customer", err) return } for i := range perDeveloper { perDeveloper[i].Key = s.nameOf(r, perDeveloper[i].Key) } for i := range perCustomer { perCustomer[i].Key = s.nameOf(r, perCustomer[i].Key) } // scope for board depth + lead time: my customers (or one) listFor := consultantID customers, err := s.store.ListCustomers(r.Context(), listFor, false) if err != nil { s.internalError(w, r, "customers", err) return } ids := []string{} depthNames := map[string]string{} for _, c := range customers { if customerID == "" || c.ID == customerID { ids = append(ids, c.ID) depthNames[c.ID] = c.Name } } leadHours, err := s.store.AtomizationLeadTimeHours(r.Context(), ids, from, to) if err != nil { s.internalError(w, r, "lead time", err) return } depth, err := s.store.OpenBoardDepth(r.Context(), ids) if err != nil { s.internalError(w, r, "board depth", err) return } depthOut := []map[string]any{} var depthTotal int64 for id, n := range depth { depthTotal += n depthOut = append(depthOut, map[string]any{"customer": depthNames[id], "open": n}) } writeJSON(w, http.StatusOK, map[string]any{ "totalBounty": total, "tasksCompleted": tasks, "weekly": weekly, "perDeveloper": perDeveloper, "perCustomer": perCustomer, "atomizationLeadHours": leadHours, "openBoardDepth": depthTotal, "openBoardByCustomer": depthOut, "customers": func() []map[string]any { out := make([]map[string]any, len(customers)) for i, c := range customers { out[i] = map[string]any{"id": c.ID, "name": c.Name} } return out }(), "from": from, "to": to, }) } // writeAwardsCSV implements §11.12 export. func (s *Server) writeAwardsCSV(w http.ResponseWriter, r *http.Request, developerID, consultantID, customerID string, from, to time.Time) { awards, err := s.store.ListAwards(r.Context(), developerID, consultantID, customerID, from, to) if err != nil { s.internalError(w, r, "list awards", err) return } w.Header().Set("Content-Type", "text/csv; charset=utf-8") w.Header().Set("Content-Disposition", `attachment; filename="bounty-awards.csv"`) cw := csv.NewWriter(w) cw.Write([]string{"awardedAt", "taskId", "developer", "consultant", "customer", "amount", "coefficient"}) for _, a := range awards { cw.Write([]string{ a.AwardedAt.Format(time.RFC3339), a.TaskID, s.nameOf(r, a.DeveloperID), s.nameOf(r, a.ConsultantID), s.nameOf(r, a.CustomerID), strconv.FormatFloat(a.Amount, 'f', 2, 64), strconv.FormatFloat(a.Coefficient, 'f', 4, 64), }) } cw.Flush() } // handleLeaderboard: top developers by bounty, honoring opt-out (§11.8). func (s *Server) handleLeaderboard(w http.ResponseWriter, r *http.Request) { from, to := metricsWindow(r) rows, err := s.store.Leaderboard(r.Context(), from, to, 10) if err != nil { s.internalError(w, r, "leaderboard", err) return } out := []map[string]any{} for _, row := range rows { if len(out) >= 10 { break } u, err := s.store.UserByID(r.Context(), row.Key) if err != nil { continue } if u.Settings.LeaderboardOptOut { continue } out = append(out, map[string]any{ "developerId": u.ID, "name": u.Name, "avatarFileId": u.AvatarFileID, "amount": row.Amount, "tasks": row.Tasks, }) } writeJSON(w, http.StatusOK, map[string]any{"leaderboard": out, "from": from, "to": to}) }