package main import ( "database/sql" "fmt" "net/http" "strconv" "time" "github.com/gin-gonic/gin" ) // CreateCounterHandler creates a new counter func CreateCounterHandler(c *gin.Context) { var req CreateCounterRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID := c.GetInt("user_id") var counter Counter err := db.QueryRow( "INSERT INTO counters (user_id, name, description) VALUES ($1, $2, $3) RETURNING id, user_id, name, description, created_at, updated_at", userID, req.Name, req.Description, ).Scan(&counter.ID, &counter.UserID, &counter.Name, &counter.Description, &counter.CreatedAt, &counter.UpdatedAt) if err != nil { RecordDBOperation("insert", "counters") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create counter"}) return } RecordDBOperation("insert", "counters") c.JSON(http.StatusCreated, counter) } // GetCountersHandler retrieves all counters for the authenticated user func GetCountersHandler(c *gin.Context) { userID := c.GetInt("user_id") search := c.Query("search") query := ` SELECT c.id, c.user_id, c.name, c.description, c.created_at, c.updated_at, COALESCE(SUM(ce.value), 0) as total_value, COALESCE(SUM(CASE WHEN ce.date = CURRENT_DATE THEN ce.value ELSE 0 END), 0) as today_value, COALESCE(SUM(CASE WHEN ce.date >= CURRENT_DATE - INTERVAL '7 days' THEN ce.value ELSE 0 END), 0) as week_value, COALESCE(SUM(CASE WHEN ce.date >= DATE_TRUNC('month', CURRENT_DATE) THEN ce.value ELSE 0 END), 0) as month_value, COUNT(ce.id) as entry_count FROM counters c LEFT JOIN counter_entries ce ON c.id = ce.counter_id WHERE c.user_id = $1 ` args := []interface{}{userID} if search != "" { query += " AND (c.name ILIKE $2 OR c.description ILIKE $2)" args = append(args, "%"+search+"%") } query += " GROUP BY c.id, c.user_id, c.name, c.description, c.created_at, c.updated_at ORDER BY c.updated_at DESC" rows, err := db.Query(query, args...) if err != nil { RecordDBOperation("select", "counters") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch counters"}) return } defer rows.Close() RecordDBOperation("select", "counters") counters := []CounterWithStats{} // Initialize as empty slice, not nil for rows.Next() { var counter CounterWithStats err := rows.Scan( &counter.ID, &counter.UserID, &counter.Name, &counter.Description, &counter.CreatedAt, &counter.UpdatedAt, &counter.TotalValue, &counter.TodayValue, &counter.WeekValue, &counter.MonthValue, &counter.EntryCount, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan counter"}) return } counters = append(counters, counter) } c.JSON(http.StatusOK, counters) } // GetCounterHandler retrieves a specific counter by ID func GetCounterHandler(c *gin.Context) { userID := c.GetInt("user_id") counterID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid counter ID"}) return } var counter CounterWithStats err = db.QueryRow(` SELECT c.id, c.user_id, c.name, c.description, c.created_at, c.updated_at, COALESCE(SUM(ce.value), 0) as total_value, COALESCE(SUM(CASE WHEN ce.date = CURRENT_DATE THEN ce.value ELSE 0 END), 0) as today_value, COALESCE(SUM(CASE WHEN ce.date >= CURRENT_DATE - INTERVAL '7 days' THEN ce.value ELSE 0 END), 0) as week_value, COALESCE(SUM(CASE WHEN ce.date >= DATE_TRUNC('month', CURRENT_DATE) THEN ce.value ELSE 0 END), 0) as month_value, COUNT(ce.id) as entry_count FROM counters c LEFT JOIN counter_entries ce ON c.id = ce.counter_id WHERE c.id = $1 AND c.user_id = $2 GROUP BY c.id, c.user_id, c.name, c.description, c.created_at, c.updated_at `, counterID, userID).Scan( &counter.ID, &counter.UserID, &counter.Name, &counter.Description, &counter.CreatedAt, &counter.UpdatedAt, &counter.TotalValue, &counter.TodayValue, &counter.WeekValue, &counter.MonthValue, &counter.EntryCount, ) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "Counter not found"}) return } else if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch counter"}) return } c.JSON(http.StatusOK, counter) } // UpdateCounterHandler updates a counter func UpdateCounterHandler(c *gin.Context) { userID := c.GetInt("user_id") counterID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid counter ID"}) return } var req UpdateCounterRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var counter Counter err = db.QueryRow( "UPDATE counters SET name = $1, description = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $3 AND user_id = $4 RETURNING id, user_id, name, description, created_at, updated_at", req.Name, req.Description, counterID, userID, ).Scan(&counter.ID, &counter.UserID, &counter.Name, &counter.Description, &counter.CreatedAt, &counter.UpdatedAt) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "Counter not found"}) return } else if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update counter"}) return } c.JSON(http.StatusOK, counter) } // DeleteCounterHandler deletes a counter func DeleteCounterHandler(c *gin.Context) { userID := c.GetInt("user_id") counterID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid counter ID"}) return } result, err := db.Exec("DELETE FROM counters WHERE id = $1 AND user_id = $2", counterID, userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete counter"}) return } rowsAffected, err := result.RowsAffected() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check deletion"}) return } if rowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "Counter not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "Counter deleted successfully"}) } // IncrementCounterHandler increments/decrements a counter func IncrementCounterHandler(c *gin.Context) { userID := c.GetInt("user_id") counterID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid counter ID"}) return } var req IncrementRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Verify counter belongs to user var exists bool err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM counters WHERE id = $1 AND user_id = $2)", counterID, userID).Scan(&exists) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) return } if !exists { c.JSON(http.StatusNotFound, gin.H{"error": "Counter not found"}) return } // Insert counter entry var entry CounterEntry err = db.QueryRow( "INSERT INTO counter_entries (counter_id, value, date) VALUES ($1, $2, CURRENT_DATE) RETURNING id, counter_id, value, date, created_at", counterID, req.Value, ).Scan(&entry.ID, &entry.CounterID, &entry.Value, &entry.Date, &entry.CreatedAt) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create counter entry"}) return } // Update counter's updated_at timestamp _, err = db.Exec("UPDATE counters SET updated_at = CURRENT_TIMESTAMP WHERE id = $1", counterID) if err != nil { // Log error but don't fail the request fmt.Printf("Warning: Failed to update counter timestamp: %v\n", err) } c.JSON(http.StatusCreated, entry) } // GetCounterEntriesHandler retrieves entries for a specific counter func GetCounterEntriesHandler(c *gin.Context) { userID := c.GetInt("user_id") counterID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid counter ID"}) return } // Verify counter belongs to user var exists bool err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM counters WHERE id = $1 AND user_id = $2)", counterID, userID).Scan(&exists) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) return } if !exists { c.JSON(http.StatusNotFound, gin.H{"error": "Counter not found"}) return } // Parse date range parameters startDate := c.Query("start_date") endDate := c.Query("end_date") query := ` SELECT id, counter_id, value, date, created_at FROM counter_entries WHERE counter_id = $1 ` args := []interface{}{counterID} if startDate != "" { query += " AND date >= $2" args = append(args, startDate) if endDate != "" { query += " AND date <= $3" args = append(args, endDate) } } else if endDate != "" { query += " AND date <= $2" args = append(args, endDate) } query += " ORDER BY date DESC, created_at DESC" rows, err := db.Query(query, args...) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch counter entries"}) return } defer rows.Close() var entries []CounterEntry for rows.Next() { var entry CounterEntry err := rows.Scan(&entry.ID, &entry.CounterID, &entry.Value, &entry.Date, &entry.CreatedAt) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan entry"}) return } entries = append(entries, entry) } c.JSON(http.StatusOK, entries) } // GetCounterStatsHandler retrieves statistics for a counter func GetCounterStatsHandler(c *gin.Context) { userID := c.GetInt("user_id") counterID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid counter ID"}) return } // Verify counter belongs to user var exists bool err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM counters WHERE id = $1 AND user_id = $2)", counterID, userID).Scan(&exists) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) return } if !exists { c.JSON(http.StatusNotFound, gin.H{"error": "Counter not found"}) return } // Get daily statistics for the last 30 days rows, err := db.Query(` SELECT date, SUM(value) as daily_total FROM counter_entries WHERE counter_id = $1 AND date >= CURRENT_DATE - INTERVAL '30 days' GROUP BY date ORDER BY date DESC `, counterID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch statistics"}) return } defer rows.Close() type DailyStat struct { Date time.Time `json:"date"` Total int `json:"total"` } var stats []DailyStat for rows.Next() { var stat DailyStat err := rows.Scan(&stat.Date, &stat.Total) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan statistic"}) return } stats = append(stats, stat) } c.JSON(http.StatusOK, gin.H{"daily_stats": stats}) }