357 lines
11 KiB
Go
357 lines
11 KiB
Go
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 {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create counter"})
|
|
return
|
|
}
|
|
|
|
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 {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch counters"})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var counters []CounterWithStats
|
|
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})
|
|
}
|