Files
counter/counters.go
aovantsev 324e861218
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
basic frontend
2025-10-03 11:32:59 +03:00

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})
}