mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-06-03 20:12:58 +00:00
329 lines
9.6 KiB
Go
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})
|
|
}
|