lots of stuff, don't truly remember

This commit is contained in:
Blake Ridgway
2026-05-17 20:39:47 -05:00
parent 178ffb3425
commit dc4fe558b7
35 changed files with 3501 additions and 112 deletions

210
internal/goal/handler.go Normal file
View File

@@ -0,0 +1,210 @@
package goal
import (
"encoding/json"
"net/http"
"strconv"
"time"
"rideaware/internal/config"
"rideaware/internal/middleware"
)
type Handler struct{}
func NewHandler() *Handler {
return &Handler{}
}
// CreateGoal POST /api/protected/goals
func (h *Handler) CreateGoal(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims)
var req struct {
Title string `json:"title"`
Description string `json:"description"`
GoalType string `json:"goal_type"`
TargetValue float64 `json:"target_value"`
TargetDate *time.Time `json:"target_date"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Title == "" {
writeError(w, http.StatusBadRequest, "title is required")
return
}
if req.TargetValue <= 0 {
writeError(w, http.StatusBadRequest, "target_value must be greater than 0")
return
}
// Validate goal type
gType := GoalType(req.GoalType)
switch gType {
case GoalDistance, GoalFrequency, GoalPower, GoalWeight, GoalCustom, "":
if gType == "" {
gType = GoalCustom
}
default:
writeError(w, http.StatusBadRequest, "invalid goal_type")
return
}
goal := &Goal{
UserID: claims.UserID,
Title: req.Title,
Description: req.Description,
GoalType: gType,
TargetValue: req.TargetValue,
TargetDate: req.TargetDate,
Status: StatusActive,
}
repo := NewRepository()
if err := repo.Create(goal); err != nil {
writeError(w, http.StatusInternalServerError, "failed to create goal")
return
}
writeJSON(w, http.StatusCreated, goal)
}
// GetGoals GET /api/protected/goals?status=active
func (h *Handler) GetGoals(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims)
statusFilter := r.URL.Query().Get("status")
repo := NewRepository()
var goals []Goal
var err error
if statusFilter != "" {
goals, err = repo.ListByStatus(claims.UserID, GoalStatus(statusFilter))
} else {
goals, err = repo.List(claims.UserID)
}
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to fetch goals")
return
}
if goals == nil {
goals = []Goal{}
}
writeJSON(w, http.StatusOK, goals)
}
// UpdateGoal PUT /api/protected/goals?id=X
func (h *Handler) UpdateGoal(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims)
idStr := r.URL.Query().Get("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid goal id")
return
}
repo := NewRepository()
goal, err := repo.GetByID(uint(id), claims.UserID)
if err != nil {
writeError(w, http.StatusNotFound, "goal not found")
return
}
var req struct {
Title string `json:"title"`
Description string `json:"description"`
GoalType string `json:"goal_type"`
TargetValue *float64 `json:"target_value"`
CurrentValue *float64 `json:"current_value"`
TargetDate *time.Time `json:"target_date"`
Status string `json:"status"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Title != "" {
goal.Title = req.Title
}
if req.Description != "" {
goal.Description = req.Description
}
if req.GoalType != "" {
gType := GoalType(req.GoalType)
switch gType {
case GoalDistance, GoalFrequency, GoalPower, GoalWeight, GoalCustom:
goal.GoalType = gType
default:
writeError(w, http.StatusBadRequest, "invalid goal_type")
return
}
}
if req.TargetValue != nil {
goal.TargetValue = *req.TargetValue
}
if req.CurrentValue != nil {
goal.CurrentValue = *req.CurrentValue
}
if req.TargetDate != nil {
goal.TargetDate = req.TargetDate
}
if req.Status != "" {
switch GoalStatus(req.Status) {
case StatusActive, StatusCompleted, StatusArchived:
goal.Status = GoalStatus(req.Status)
default:
writeError(w, http.StatusBadRequest, "invalid status")
return
}
}
if err := repo.Update(goal); err != nil {
writeError(w, http.StatusInternalServerError, "failed to update goal")
return
}
writeJSON(w, http.StatusOK, goal)
}
// DeleteGoal DELETE /api/protected/goals?id=X
func (h *Handler) DeleteGoal(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims)
idStr := r.URL.Query().Get("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid goal id")
return
}
repo := NewRepository()
if err := repo.Delete(uint(id), claims.UserID); err != nil {
writeError(w, http.StatusNotFound, "goal not found")
return
}
w.WriteHeader(http.StatusNoContent)
}
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{"error": message})
}

42
internal/goal/model.go Normal file
View File

@@ -0,0 +1,42 @@
package goal
import "time"
// GoalType defines categories of goals users can set.
type GoalType string
const (
GoalDistance GoalType = "distance"
GoalFrequency GoalType = "frequency"
GoalPower GoalType = "power"
GoalWeight GoalType = "weight"
GoalCustom GoalType = "custom"
)
// GoalStatus tracks the lifecycle of a goal.
type GoalStatus string
const (
StatusActive GoalStatus = "active"
StatusCompleted GoalStatus = "completed"
StatusArchived GoalStatus = "archived"
)
type Goal struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"not null;index" json:"user_id"`
Title string `gorm:"not null" json:"title"`
Description string `gorm:"default:''" json:"description"`
GoalType GoalType `gorm:"default:'custom'" json:"goal_type"`
TargetValue float64 `gorm:"not null;default:0" json:"target_value"`
CurrentValue float64 `gorm:"default:0" json:"current_value"`
TargetDate *time.Time `json:"target_date,omitempty"`
Status GoalStatus `gorm:"default:'active'" json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (Goal) TableName() string {
return "goals"
}

View File

@@ -0,0 +1,49 @@
package goal
import (
"rideaware/pkg/database"
)
type Repository struct{}
func NewRepository() *Repository {
return &Repository{}
}
func (r *Repository) Create(goal *Goal) error {
return database.DB.Create(goal).Error
}
func (r *Repository) GetByID(id, userID uint) (*Goal, error) {
var g Goal
err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&g).Error
if err != nil {
return nil, err
}
return &g, nil
}
func (r *Repository) List(userID uint) ([]Goal, error) {
var goals []Goal
err := database.DB.Where("user_id = ?", userID).
Order("created_at desc").
Find(&goals).Error
return goals, err
}
func (r *Repository) ListByStatus(userID uint, status GoalStatus) ([]Goal, error) {
var goals []Goal
err := database.DB.Where("user_id = ? AND status = ?", userID, status).
Order("created_at desc").
Find(&goals).Error
return goals, err
}
func (r *Repository) Update(goal *Goal) error {
return database.DB.Save(goal).Error
}
func (r *Repository) Delete(id, userID uint) error {
return database.DB.Where("id = ? AND user_id = ?", id, userID).Delete(&Goal{}).Error
}