Files
Trackeep/backend/handlers/goals_habits.go
T
Tomas Dvorak d27cf14110 first test
2026-02-08 14:14:55 +01:00

569 lines
16 KiB
Go

package handlers
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/trackeep/backend/models"
"gorm.io/gorm"
)
// GoalsHabitsHandler handles goals and habits operations
type GoalsHabitsHandler struct {
db *gorm.DB
}
// NewGoalsHabitsHandler creates a new goals and habits handler
func NewGoalsHabitsHandler(db *gorm.DB) *GoalsHabitsHandler {
return &GoalsHabitsHandler{db: db}
}
// Goal Handlers
// CreateGoal creates a new goal
func (h *GoalsHabitsHandler) CreateGoal(c *gin.Context) {
userID := c.GetUint("user_id")
var req struct {
Title string `json:"title" binding:"required"`
Description string `json:"description"`
Category string `json:"category"`
TargetValue float64 `json:"target_value"`
Unit string `json:"unit"`
Deadline time.Time `json:"deadline"`
Priority string `json:"priority"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
goal := models.Goal{
UserID: userID,
Title: req.Title,
Description: req.Description,
Category: req.Category,
TargetValue: req.TargetValue,
CurrentValue: 0,
Unit: req.Unit,
Deadline: req.Deadline,
Status: "active",
Priority: req.Priority,
Progress: 0,
IsCompleted: false,
}
if err := h.db.Create(&goal).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create goal"})
return
}
c.JSON(http.StatusCreated, goal)
}
// GetGoals retrieves user's goals
func (h *GoalsHabitsHandler) GetGoals(c *gin.Context) {
userID := c.GetUint("user_id")
category := c.Query("category")
status := c.Query("status")
priority := c.Query("priority")
limit := 20
if l := c.Query("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 {
limit = parsed
}
}
query := h.db.Where("user_id = ?", userID)
if category != "" {
query = query.Where("category = ?", category)
}
if status != "" {
query = query.Where("status = ?", status)
}
if priority != "" {
query = query.Where("priority = ?", priority)
}
var goals []models.Goal
if err := query.Preload("Milestones").Order("created_at DESC").Limit(limit).Find(&goals).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch goals"})
return
}
c.JSON(http.StatusOK, gin.H{
"goals": goals,
"limit": limit,
})
}
// GetGoal retrieves a specific goal
func (h *GoalsHabitsHandler) GetGoal(c *gin.Context) {
userID := c.GetUint("user_id")
goalID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"})
return
}
var goal models.Goal
if err := h.db.Where("id = ? AND user_id = ?", goalID, userID).
Preload("Milestones").
First(&goal).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"})
return
}
c.JSON(http.StatusOK, goal)
}
// UpdateGoal updates a goal
func (h *GoalsHabitsHandler) UpdateGoal(c *gin.Context) {
userID := c.GetUint("user_id")
goalID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"})
return
}
var goal models.Goal
if err := h.db.Where("id = ? AND user_id = ?", goalID, userID).First(&goal).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"})
return
}
var req struct {
Title string `json:"title"`
Description string `json:"description"`
Category string `json:"category"`
TargetValue float64 `json:"target_value"`
CurrentValue float64 `json:"current_value"`
Unit string `json:"unit"`
Deadline time.Time `json:"deadline"`
Status string `json:"status"`
Priority string `json:"priority"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Update fields
if req.Title != "" {
goal.Title = req.Title
}
if req.Description != "" {
goal.Description = req.Description
}
if req.Category != "" {
goal.Category = req.Category
}
if req.TargetValue > 0 {
goal.TargetValue = req.TargetValue
}
if req.CurrentValue >= 0 {
goal.CurrentValue = req.CurrentValue
}
if req.Unit != "" {
goal.Unit = req.Unit
}
if !req.Deadline.IsZero() {
goal.Deadline = req.Deadline
}
if req.Status != "" {
goal.Status = req.Status
}
if req.Priority != "" {
goal.Priority = req.Priority
}
if err := h.db.Save(&goal).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update goal"})
return
}
c.JSON(http.StatusOK, goal)
}
// DeleteGoal deletes a goal
func (h *GoalsHabitsHandler) DeleteGoal(c *gin.Context) {
userID := c.GetUint("user_id")
goalID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"})
return
}
var goal models.Goal
if err := h.db.Where("id = ? AND user_id = ?", goalID, userID).First(&goal).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"})
return
}
if err := h.db.Delete(&goal).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete goal"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Goal deleted successfully"})
}
// Habit Handlers
// CreateHabit creates a new habit
func (h *GoalsHabitsHandler) CreateHabit(c *gin.Context) {
userID := c.GetUint("user_id")
var req struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Category string `json:"category"`
TargetFrequency int `json:"target_frequency"`
FrequencyUnit string `json:"frequency_unit"`
TargetValue float64 `json:"target_value"`
Unit string `json:"unit"`
TimeOfDay string `json:"time_of_day"`
DaysOfWeek []string `json:"days_of_week"`
IsActive bool `json:"is_active"`
IsPublic bool `json:"is_public"`
GoalID *uint `json:"goal_id"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
habit := models.Habit{
UserID: userID,
Name: req.Name,
Description: req.Description,
Category: req.Category,
TargetFrequency: req.TargetFrequency,
FrequencyUnit: req.FrequencyUnit,
TargetValue: req.TargetValue,
Unit: req.Unit,
TimeOfDay: req.TimeOfDay,
DaysOfWeek: req.DaysOfWeek,
IsActive: req.IsActive,
IsPublic: req.IsPublic,
GoalID: req.GoalID,
Streak: 0,
LongestStreak: 0,
CompletionRate: 0,
}
if err := h.db.Create(&habit).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create habit"})
return
}
c.JSON(http.StatusCreated, habit)
}
// GetHabits retrieves user's habits
func (h *GoalsHabitsHandler) GetHabits(c *gin.Context) {
userID := c.GetUint("user_id")
category := c.Query("category")
isActive := c.Query("is_active")
limit := 20
if l := c.Query("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 {
limit = parsed
}
}
query := h.db.Where("user_id = ?", userID)
if category != "" {
query = query.Where("category = ?", category)
}
if isActive != "" {
active := isActive == "true"
query = query.Where("is_active = ?", active)
}
var habits []models.Habit
if err := query.Preload("Goal").Order("created_at DESC").Limit(limit).Find(&habits).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch habits"})
return
}
c.JSON(http.StatusOK, gin.H{
"habits": habits,
"limit": limit,
})
}
// GetHabit retrieves a specific habit
func (h *GoalsHabitsHandler) GetHabit(c *gin.Context) {
userID := c.GetUint("user_id")
habitID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid habit ID"})
return
}
var habit models.Habit
if err := h.db.Where("id = ? AND user_id = ?", habitID, userID).
Preload("Goal").
Preload("HabitEntries", "entry_date >= ?", time.Now().AddDate(0, 0, -30)).
First(&habit).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Habit not found"})
return
}
c.JSON(http.StatusOK, habit)
}
// UpdateHabit updates a habit
func (h *GoalsHabitsHandler) UpdateHabit(c *gin.Context) {
userID := c.GetUint("user_id")
habitID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid habit ID"})
return
}
var habit models.Habit
if err := h.db.Where("id = ? AND user_id = ?", habitID, userID).First(&habit).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Habit not found"})
return
}
var req struct {
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
TargetFrequency int `json:"target_frequency"`
FrequencyUnit string `json:"frequency_unit"`
TargetValue float64 `json:"target_value"`
Unit string `json:"unit"`
TimeOfDay string `json:"time_of_day"`
DaysOfWeek []string `json:"days_of_week"`
IsActive bool `json:"is_active"`
IsPublic bool `json:"is_public"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Update fields
if req.Name != "" {
habit.Name = req.Name
}
if req.Description != "" {
habit.Description = req.Description
}
if req.Category != "" {
habit.Category = req.Category
}
if req.TargetFrequency > 0 {
habit.TargetFrequency = req.TargetFrequency
}
if req.FrequencyUnit != "" {
habit.FrequencyUnit = req.FrequencyUnit
}
if req.TargetValue > 0 {
habit.TargetValue = req.TargetValue
}
if req.Unit != "" {
habit.Unit = req.Unit
}
if req.TimeOfDay != "" {
habit.TimeOfDay = req.TimeOfDay
}
if req.DaysOfWeek != nil {
habit.DaysOfWeek = req.DaysOfWeek
}
habit.IsActive = req.IsActive
habit.IsPublic = req.IsPublic
if err := h.db.Save(&habit).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update habit"})
return
}
c.JSON(http.StatusOK, habit)
}
// DeleteHabit deletes a habit
func (h *GoalsHabitsHandler) DeleteHabit(c *gin.Context) {
userID := c.GetUint("user_id")
habitID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid habit ID"})
return
}
var habit models.Habit
if err := h.db.Where("id = ? AND user_id = ?", habitID, userID).First(&habit).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Habit not found"})
return
}
if err := h.db.Delete(&habit).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete habit"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Habit deleted successfully"})
}
// HabitEntry Handlers
// CreateHabitEntry creates a new habit entry
func (h *GoalsHabitsHandler) CreateHabitEntry(c *gin.Context) {
userID := c.GetUint("user_id")
var req struct {
HabitID uint `json:"habit_id" binding:"required"`
EntryDate time.Time `json:"entry_date" binding:"required"`
Value float64 `json:"value"`
TargetValue float64 `json:"target_value"`
Unit string `json:"unit"`
Notes string `json:"notes"`
Quality int `json:"quality"`
TimeSpent int `json:"time_spent"`
Location string `json:"location"`
Mood string `json:"mood"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Verify habit ownership
var habit models.Habit
if err := h.db.Where("id = ? AND user_id = ?", req.HabitID, userID).First(&habit).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Habit not found"})
return
}
entry := models.HabitEntry{
HabitID: req.HabitID,
EntryDate: req.EntryDate,
Value: req.Value,
TargetValue: req.TargetValue,
Unit: req.Unit,
Notes: req.Notes,
Quality: req.Quality,
TimeSpent: req.TimeSpent,
Location: req.Location,
Mood: req.Mood,
}
if err := h.db.Create(&entry).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create habit entry"})
return
}
c.JSON(http.StatusCreated, entry)
}
// GetHabitEntries retrieves habit entries
func (h *GoalsHabitsHandler) GetHabitEntries(c *gin.Context) {
userID := c.GetUint("user_id")
habitID, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid habit ID"})
return
}
// Verify habit ownership
var habit models.Habit
if err := h.db.Where("id = ? AND user_id = ?", habitID, userID).First(&habit).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Habit not found"})
return
}
startDate := c.Query("start_date")
endDate := c.Query("end_date")
limit := 50
if l := c.Query("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 {
limit = parsed
}
}
query := h.db.Where("habit_id = ?", habitID)
if startDate != "" {
if parsed, err := time.Parse("2006-01-02", startDate); err == nil {
query = query.Where("entry_date >= ?", parsed)
}
}
if endDate != "" {
if parsed, err := time.Parse("2006-01-02", endDate); err == nil {
query = query.Where("entry_date <= ?", parsed)
}
}
var entries []models.HabitEntry
if err := query.Order("entry_date DESC").Limit(limit).Find(&entries).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch habit entries"})
return
}
c.JSON(http.StatusOK, gin.H{
"entries": entries,
"limit": limit,
})
}
// GetDashboardStats retrieves dashboard statistics for goals and habits
func (h *GoalsHabitsHandler) GetDashboardStats(c *gin.Context) {
userID := c.GetUint("user_id")
// Goal stats
var totalGoals, activeGoals, completedGoals int64
h.db.Model(&models.Goal{}).Where("user_id = ?", userID).Count(&totalGoals)
h.db.Model(&models.Goal{}).Where("user_id = ? AND status = ?", userID, "active").Count(&activeGoals)
h.db.Model(&models.Goal{}).Where("user_id = ? AND status = ?", userID, "completed").Count(&completedGoals)
// Habit stats
var totalHabits, activeHabits int64
h.db.Model(&models.Habit{}).Where("user_id = ?", userID).Count(&totalHabits)
h.db.Model(&models.Habit{}).Where("user_id = ? AND is_active = ?", userID, true).Count(&activeHabits)
// Today's habit entries
today := time.Now().Truncate(24 * time.Hour)
tomorrow := today.Add(24 * time.Hour)
var todayEntries int64
h.db.Model(&models.HabitEntry{}).
Joins("JOIN habits ON habit_entries.habit_id = habits.id").
Where("habits.user_id = ? AND habit_entries.entry_date >= ? AND habit_entries.entry_date < ?", userID, today, tomorrow).
Count(&todayEntries)
// Current week streak
weekAgo := time.Now().AddDate(0, 0, -7)
var weekEntries int64
h.db.Model(&models.HabitEntry{}).
Joins("JOIN habits ON habit_entries.habit_id = habits.id").
Where("habits.user_id = ? AND habit_entries.entry_date >= ? AND habit_entries.is_completed = ?", userID, weekAgo, true).
Count(&weekEntries)
stats := gin.H{
"goals": gin.H{
"total": totalGoals,
"active": activeGoals,
"completed": completedGoals,
},
"habits": gin.H{
"total": totalHabits,
"active": activeHabits,
"today_entries": todayEntries,
"week_streak": weekEntries,
},
}
c.JSON(http.StatusOK, stats)
}