package server import ( "net/http" "task/internal/sqlite" "task/internal/ticket" ) func (s *Server) handleProjectReport(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if _, ok, code := s.requireProjectRole(id, sqlite.RoleViewer, r); !ok { writeError(w, code, "forbidden") return } groupBy := ticket.GroupBy(r.URL.Query().Get("groupBy")) if groupBy == "" { groupBy = ticket.GroupByTicket } valid := false for _, g := range ticket.AllGroupBys { if g == groupBy { valid = true break } } if !valid { writeError(w, http.StatusBadRequest, "invalid groupBy") return } entries, err := s.Times.ListByProject(id) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } tickets, err := s.Tickets.List(id) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } byID := make(map[string]ticket.Ticket, len(tickets)) for _, t := range tickets { byID[t.ID] = t } rows := ticket.Aggregate(entries, byID, groupBy) total := ticket.TotalDuration(entries) type rowJSON struct { Key string `json:"key"` Label string `json:"label"` DurationNs int64 `json:"durationNs"` } out := make([]rowJSON, 0, len(rows)) for _, row := range rows { out = append(out, rowJSON{ Key: row.Key, Label: row.Label, DurationNs: int64(row.Duration), }) } writeJSON(w, http.StatusOK, map[string]any{ "groupBy": groupBy, "totalDurationNs": int64(total), "rows": out, "entryCount": len(entries), }) }