Files
Tomas Dvorak d27cf14110 first test
2026-02-08 14:14:55 +01:00

329 lines
9.6 KiB
Go

package handlers
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/trackeep/backend/models"
"gorm.io/gorm"
)
// TimeEntryHandler handles time tracking operations
type TimeEntryHandler struct {
db *gorm.DB
}
// NewTimeEntryHandler creates a new time entry handler
func NewTimeEntryHandler(db *gorm.DB) *TimeEntryHandler {
return &TimeEntryHandler{db: db}
}
// CreateTimeEntryRequest represents the request to create a time entry
type CreateTimeEntryRequest struct {
TaskID *uint `json:"task_id,omitempty"`
BookmarkID *uint `json:"bookmark_id,omitempty"`
NoteID *uint `json:"note_id,omitempty"`
Description string `json:"description"`
Tags []string `json:"tags,omitempty"`
Billable bool `json:"billable"`
HourlyRate *float64 `json:"hourly_rate,omitempty"`
Source string `json:"source" gorm:"default:manual"`
}
// UpdateTimeEntryRequest represents the request to update a time entry
type UpdateTimeEntryRequest struct {
Description *string `json:"description,omitempty"`
Tags []string `json:"tags,omitempty"`
Billable *bool `json:"billable,omitempty"`
HourlyRate *float64 `json:"hourly_rate,omitempty"`
EndTime *time.Time `json:"end_time,omitempty"`
}
// GetTimeEntries retrieves all time entries for the authenticated user
func (h *TimeEntryHandler) GetTimeEntries(c *gin.Context) {
userID := c.GetUint("user_id")
var timeEntries []models.TimeEntry
query := h.db.Where("user_id = ?", userID).
Preload("Task").
Preload("Bookmark").
Preload("Note").
Preload("Tags").
Order("created_at DESC")
// Filter by date range if provided
if startDate := c.Query("start_date"); startDate != "" {
if parsed, err := time.Parse("2006-01-02", startDate); err == nil {
query = query.Where("start_time >= ?", parsed)
}
}
if endDate := c.Query("end_date"); endDate != "" {
if parsed, err := time.Parse("2006-01-02", endDate); err == nil {
query = query.Where("start_time <= ?", parsed.Add(24*time.Hour))
}
}
// Filter by running status
if isRunning := c.Query("is_running"); isRunning != "" {
running := isRunning == "true"
query = query.Where("is_running = ?", running)
}
if err := query.Find(&timeEntries).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve time entries"})
return
}
c.JSON(http.StatusOK, gin.H{"time_entries": timeEntries})
}
// GetTimeEntry retrieves a specific time entry
func (h *TimeEntryHandler) GetTimeEntry(c *gin.Context) {
userID := c.GetUint("user_id")
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid time entry ID"})
return
}
var timeEntry models.TimeEntry
if err := h.db.Where("id = ? AND user_id = ?", id, userID).
Preload("Task").
Preload("Bookmark").
Preload("Note").
Preload("Tags").
First(&timeEntry).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Time entry not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve time entry"})
return
}
c.JSON(http.StatusOK, gin.H{"time_entry": timeEntry})
}
// CreateTimeEntry creates a new time entry
func (h *TimeEntryHandler) CreateTimeEntry(c *gin.Context) {
userID := c.GetUint("user_id")
var req CreateTimeEntryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
timeEntry := models.TimeEntry{
UserID: userID,
TaskID: req.TaskID,
BookmarkID: req.BookmarkID,
NoteID: req.NoteID,
Description: req.Description,
Billable: req.Billable,
HourlyRate: req.HourlyRate,
Source: req.Source,
StartTime: time.Now(),
IsRunning: true,
}
// Handle tags
if len(req.Tags) > 0 {
var tags []models.Tag
for _, tagName := range req.Tags {
var tag models.Tag
if err := h.db.Where("name = ?", tagName).FirstOrCreate(&tag, models.Tag{Name: tagName}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create tags"})
return
}
tags = append(tags, tag)
}
timeEntry.Tags = tags
}
if err := h.db.Create(&timeEntry).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create time entry"})
return
}
// Load relationships for response
h.db.Preload("Task").Preload("Bookmark").Preload("Note").Preload("Tags").First(&timeEntry)
c.JSON(http.StatusCreated, gin.H{"time_entry": timeEntry})
}
// UpdateTimeEntry updates an existing time entry
func (h *TimeEntryHandler) UpdateTimeEntry(c *gin.Context) {
userID := c.GetUint("user_id")
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid time entry ID"})
return
}
var timeEntry models.TimeEntry
if err := h.db.Where("id = ? AND user_id = ?", id, userID).First(&timeEntry).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Time entry not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve time entry"})
return
}
var req UpdateTimeEntryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Update fields
if req.Description != nil {
timeEntry.Description = *req.Description
}
if req.Billable != nil {
timeEntry.Billable = *req.Billable
}
if req.HourlyRate != nil {
timeEntry.HourlyRate = req.HourlyRate
}
if req.EndTime != nil {
timeEntry.EndTime = req.EndTime
timeEntry.IsRunning = false
}
// Handle tags
if req.Tags != nil {
// Clear existing tags
h.db.Model(&timeEntry).Association("Tags").Clear()
// Add new tags
var tags []models.Tag
for _, tagName := range req.Tags {
var tag models.Tag
if err := h.db.Where("name = ?", tagName).FirstOrCreate(&tag, models.Tag{Name: tagName}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create tags"})
return
}
tags = append(tags, tag)
}
timeEntry.Tags = tags
}
if err := h.db.Save(&timeEntry).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update time entry"})
return
}
// Load relationships for response
h.db.Preload("Task").Preload("Bookmark").Preload("Note").Preload("Tags").First(&timeEntry)
c.JSON(http.StatusOK, gin.H{"time_entry": timeEntry})
}
// StopTimeEntry stops a running time entry
func (h *TimeEntryHandler) StopTimeEntry(c *gin.Context) {
userID := c.GetUint("user_id")
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid time entry ID"})
return
}
var timeEntry models.TimeEntry
if err := h.db.Where("id = ? AND user_id = ?", id, userID).First(&timeEntry).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Time entry not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve time entry"})
return
}
if !timeEntry.IsRunning {
c.JSON(http.StatusBadRequest, gin.H{"error": "Time entry is already stopped"})
return
}
timeEntry.Stop()
if err := h.db.Save(&timeEntry).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to stop time entry"})
return
}
// Load relationships for response
h.db.Preload("Task").Preload("Bookmark").Preload("Note").Preload("Tags").First(&timeEntry)
c.JSON(http.StatusOK, gin.H{"time_entry": timeEntry})
}
// DeleteTimeEntry deletes a time entry
func (h *TimeEntryHandler) DeleteTimeEntry(c *gin.Context) {
userID := c.GetUint("user_id")
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid time entry ID"})
return
}
var timeEntry models.TimeEntry
if err := h.db.Where("id = ? AND user_id = ?", id, userID).First(&timeEntry).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Time entry not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve time entry"})
return
}
if err := h.db.Delete(&timeEntry).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete time entry"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Time entry deleted successfully"})
}
// GetTimeStats retrieves time tracking statistics
func (h *TimeEntryHandler) GetTimeStats(c *gin.Context) {
userID := c.GetUint("user_id")
var stats struct {
TotalTimeSeconds int64 `json:"total_time_seconds"`
TotalEntries int64 `json:"total_entries"`
RunningEntries int64 `json:"running_entries"`
BillableTime int64 `json:"billable_time_seconds"`
TotalBillable float64 `json:"total_billable_amount"`
}
// Total time and entries
h.db.Model(&models.TimeEntry{}).
Where("user_id = ?", userID).
Select("COALESCE(SUM(duration), 0) as total_time_seconds, COUNT(*) as total_entries").
Scan(&stats)
// Running entries
h.db.Model(&models.TimeEntry{}).
Where("user_id = ? AND is_running = ?", userID, true).
Count(&stats.RunningEntries)
// Billable time and amount
var billableStats struct {
BillableTime int64 `json:"billable_time"`
TotalBillable float64 `json:"total_billable"`
}
h.db.Model(&models.TimeEntry{}).
Where("user_id = ? AND billable = ?", userID, true).
Select("COALESCE(SUM(duration), 0) as billable_time, COALESCE(SUM(duration * hourly_rate / 3600), 0) as total_billable").
Scan(&billableStats)
stats.BillableTime = billableStats.BillableTime
stats.TotalBillable = billableStats.TotalBillable
c.JSON(http.StatusOK, gin.H{"stats": stats})
}