package api import ( "net/http" "strconv" "github.com/go-chi/chi/v5" "geniusrun/backend/internal/store" ) // activityListItem is one row of the activities list, enriched with its // current classification so the frontend can show what kind it's assigned // to and whether that assignment is locked (see Locked's doc comment). type activityListItem struct { activityResponse WorkoutKindID *int64 `json:"workout_kind_id"` WorkoutKindName *string `json:"workout_kind_name"` AssignmentSource *string `json:"assignment_source"` AssignmentStatus *string `json:"assignment_status"` // Locked is true when a global reclassify pass will never touch this // activity again: a manual assignment is the user's definitive word, and // a Race assignment comes from a hard Garmin fact, not a retunable rule. Locked bool `json:"locked"` } func (s *Server) handleListActivities(w http.ResponseWriter, r *http.Request) { userID := userIDFromContext(r.Context()) q := r.URL.Query() filter := store.ActivityFilter{ FromDate: q.Get("from"), ToDate: q.Get("to"), } if limit, err := strconv.Atoi(q.Get("limit")); err == nil { filter.Limit = limit } if offset, err := strconv.Atoi(q.Get("offset")); err == nil { filter.Offset = offset } activities, err := s.DB.ListActivities(r.Context(), userID, filter) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } kinds, err := s.DB.ListWorkoutKinds(r.Context(), userID, false) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } kindNames := make(map[int64]string, len(kinds)) for _, k := range kinds { kindNames[k.ID] = k.Name } resp := make([]activityListItem, 0, len(activities)) for _, a := range activities { item := activityListItem{activityResponse: toActivityResponse(a)} assignment, ok, err := s.DB.CurrentAssignment(r.Context(), userID, a.ID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if ok { source, status := assignment.AssignmentSource, assignment.Status item.AssignmentSource, item.AssignmentStatus = &source, &status if assignment.WorkoutKindID != nil { item.WorkoutKindID = assignment.WorkoutKindID if name, found := kindNames[*assignment.WorkoutKindID]; found { item.WorkoutKindName = &name } } item.Locked = source == store.AssignmentSourceManual || (item.WorkoutKindName != nil && *item.WorkoutKindName == "Race") } resp = append(resp, item) } writeJSON(w, http.StatusOK, resp) } func (s *Server) handleGetActivity(w http.ResponseWriter, r *http.Request) { userID := userIDFromContext(r.Context()) id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid activity id") return } activity, ok, err := s.DB.GetActivity(r.Context(), userID, id) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if !ok { writeError(w, http.StatusNotFound, "activity not found") return } laps, err := s.DB.LapsForActivity(r.Context(), userID, id) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } assignment, hasAssignment, err := s.DB.CurrentAssignment(r.Context(), userID, id) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } resp := map[string]any{"activity": toActivityResponse(activity), "laps": toLapResponses(laps)} if hasAssignment { resp["assignment"] = assignment } writeJSON(w, http.StatusOK, resp) }