mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-06-03 20:12:58 +00:00
1044 lines
30 KiB
Go
1044 lines
30 KiB
Go
package handlers
|
|
|
|
import (
|
|
"math"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/trackeep/backend/models"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// AnalyticsHandler handles analytics operations
|
|
type AnalyticsHandler struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewAnalyticsHandler creates a new analytics handler
|
|
func NewAnalyticsHandler(db *gorm.DB) *AnalyticsHandler {
|
|
return &AnalyticsHandler{db: db}
|
|
}
|
|
|
|
// GetDashboardAnalytics returns comprehensive dashboard analytics
|
|
func (h *AnalyticsHandler) GetDashboardAnalytics(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
// Get date range from query params (default to last 30 days)
|
|
days := 30
|
|
if d := c.Query("days"); d != "" {
|
|
if parsed, err := strconv.Atoi(d); err == nil && parsed > 0 {
|
|
days = parsed
|
|
}
|
|
}
|
|
|
|
startDate := time.Now().AddDate(0, 0, -days).Truncate(24 * time.Hour)
|
|
endDate := time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour)
|
|
|
|
// Get analytics data
|
|
var analytics []models.Analytics
|
|
if err := h.db.Where("user_id = ? AND date BETWEEN ? AND ?", userID, startDate, endDate).
|
|
Order("date DESC").
|
|
Find(&analytics).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch analytics"})
|
|
return
|
|
}
|
|
|
|
// Get productivity metrics
|
|
var productivityMetrics []models.ProductivityMetrics
|
|
if err := h.db.Where("user_id = ? AND start_date BETWEEN ? AND ?", userID, startDate, endDate).
|
|
Order("start_date DESC").
|
|
Find(&productivityMetrics).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch productivity metrics"})
|
|
return
|
|
}
|
|
|
|
// Get learning analytics
|
|
var learningAnalytics []models.LearningAnalytics
|
|
if err := h.db.Where("user_id = ?", userID).
|
|
Preload("Course").
|
|
Order("last_accessed DESC").
|
|
Find(&learningAnalytics).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch learning analytics"})
|
|
return
|
|
}
|
|
|
|
// Get GitHub analytics
|
|
var githubAnalytics []models.GitHubAnalytics
|
|
if err := h.db.Where("user_id = ? AND date BETWEEN ? AND ?", userID, startDate, endDate).
|
|
Order("date DESC").
|
|
Find(&githubAnalytics).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch GitHub analytics"})
|
|
return
|
|
}
|
|
|
|
// Get active goals
|
|
var goals []models.Goal
|
|
if err := h.db.Where("user_id = ? AND status = ?", userID, "active").
|
|
Preload("Milestones").
|
|
Order("priority DESC, deadline ASC").
|
|
Find(&goals).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch goals"})
|
|
return
|
|
}
|
|
|
|
// Calculate summary statistics
|
|
totalHoursTracked := 0.0
|
|
totalTasksCompleted := 0
|
|
totalBookmarksAdded := 0
|
|
totalNotesCreated := 0
|
|
totalCoursesCompleted := 0
|
|
totalGitHubCommits := 0
|
|
|
|
for _, a := range analytics {
|
|
totalHoursTracked += a.HoursTracked
|
|
totalTasksCompleted += a.TasksCompleted
|
|
totalBookmarksAdded += a.BookmarksAdded
|
|
totalNotesCreated += a.NotesCreated
|
|
totalCoursesCompleted += a.CoursesCompleted
|
|
totalGitHubCommits += a.GitHubCommits
|
|
}
|
|
|
|
// Get habit analytics
|
|
var habitAnalytics []models.HabitAnalytics
|
|
if err := h.db.Where("user_id = ?", userID).
|
|
Order("last_completed DESC").
|
|
Find(&habitAnalytics).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch habit analytics"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"period": gin.H{
|
|
"start_date": startDate,
|
|
"end_date": endDate,
|
|
"days": days,
|
|
},
|
|
"summary": gin.H{
|
|
"hours_tracked": totalHoursTracked,
|
|
"tasks_completed": totalTasksCompleted,
|
|
"bookmarks_added": totalBookmarksAdded,
|
|
"notes_created": totalNotesCreated,
|
|
"courses_completed": totalCoursesCompleted,
|
|
"github_commits": totalGitHubCommits,
|
|
},
|
|
"analytics": analytics,
|
|
"productivity_metrics": productivityMetrics,
|
|
"learning_analytics": learningAnalytics,
|
|
"github_analytics": githubAnalytics,
|
|
"goals": goals,
|
|
"habit_analytics": habitAnalytics,
|
|
})
|
|
}
|
|
|
|
// GetProductivityMetrics returns detailed productivity metrics
|
|
func (h *AnalyticsHandler) GetProductivityMetrics(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
// Get period from query params
|
|
period := c.DefaultQuery("period", "weekly")
|
|
|
|
var startDate, endDate time.Time
|
|
now := time.Now().Truncate(24 * time.Hour)
|
|
|
|
switch period {
|
|
case "daily":
|
|
startDate = now.AddDate(0, 0, -7)
|
|
endDate = now.Add(24 * time.Hour)
|
|
case "weekly":
|
|
startDate = now.AddDate(0, 0, -28)
|
|
endDate = now.Add(24 * time.Hour)
|
|
case "monthly":
|
|
startDate = now.AddDate(0, -6, 0)
|
|
endDate = now.Add(24 * time.Hour)
|
|
case "yearly":
|
|
startDate = now.AddDate(-3, 0, 0)
|
|
endDate = now.Add(24 * time.Hour)
|
|
default:
|
|
startDate = now.AddDate(0, 0, -28)
|
|
endDate = now.Add(24 * time.Hour)
|
|
}
|
|
|
|
var metrics []models.ProductivityMetrics
|
|
if err := h.db.Where("user_id = ? AND period = ? AND start_date BETWEEN ? AND ?", userID, period, startDate, endDate).
|
|
Order("start_date DESC").
|
|
Find(&metrics).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch productivity metrics"})
|
|
return
|
|
}
|
|
|
|
// If no metrics exist, generate them
|
|
if len(metrics) == 0 {
|
|
h.generateProductivityMetrics(userID, period, startDate, endDate)
|
|
// Try again
|
|
if err := h.db.Where("user_id = ? AND period = ? AND start_date BETWEEN ? AND ?", userID, period, startDate, endDate).
|
|
Order("start_date DESC").
|
|
Find(&metrics).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch productivity metrics"})
|
|
return
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"period": period,
|
|
"metrics": metrics,
|
|
})
|
|
}
|
|
|
|
// GetLearningAnalytics returns learning progress analytics
|
|
func (h *AnalyticsHandler) GetLearningAnalytics(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
courseIDParam := c.Query("course_id")
|
|
var courseID *uint
|
|
if courseIDParam != "" {
|
|
id, err := strconv.ParseUint(courseIDParam, 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid course ID"})
|
|
return
|
|
}
|
|
courseIDUint := uint(id)
|
|
courseID = &courseIDUint
|
|
}
|
|
|
|
query := h.db.Where("user_id = ?", userID).Preload("Course")
|
|
if courseID != nil {
|
|
query = query.Where("course_id = ?", *courseID)
|
|
}
|
|
|
|
var learningAnalytics []models.LearningAnalytics
|
|
if err := query.Order("last_accessed DESC").Find(&learningAnalytics).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch learning analytics"})
|
|
return
|
|
}
|
|
|
|
// Calculate overall learning statistics
|
|
totalTimeSpent := 0.0
|
|
totalProgress := 0.0
|
|
coursesInProgress := 0
|
|
coursesCompleted := 0
|
|
|
|
for _, la := range learningAnalytics {
|
|
totalTimeSpent += la.TimeSpent
|
|
totalProgress += la.Progress
|
|
if la.Progress < 100 {
|
|
coursesInProgress++
|
|
} else {
|
|
coursesCompleted++
|
|
}
|
|
}
|
|
|
|
averageProgress := 0.0
|
|
if len(learningAnalytics) > 0 {
|
|
averageProgress = totalProgress / float64(len(learningAnalytics))
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"learning_analytics": learningAnalytics,
|
|
"summary": gin.H{
|
|
"total_time_spent": totalTimeSpent,
|
|
"average_progress": averageProgress,
|
|
"courses_in_progress": coursesInProgress,
|
|
"courses_completed": coursesCompleted,
|
|
"total_courses": len(learningAnalytics),
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetContentAnalytics returns content consumption patterns
|
|
func (h *AnalyticsHandler) GetContentAnalytics(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
contentType := c.Query("content_type")
|
|
category := c.Query("category")
|
|
days := 30
|
|
if d := c.Query("days"); d != "" {
|
|
if parsed, err := strconv.Atoi(d); err == nil && parsed > 0 {
|
|
days = parsed
|
|
}
|
|
}
|
|
|
|
startDate := time.Now().AddDate(0, 0, -days)
|
|
|
|
query := h.db.Where("user_id = ? AND last_accessed >= ?", userID, startDate).
|
|
Preload("Tags")
|
|
|
|
if contentType != "" {
|
|
query = query.Where("content_type = ?", contentType)
|
|
}
|
|
if category != "" {
|
|
query = query.Where("category = ?", category)
|
|
}
|
|
|
|
var contentAnalytics []models.ContentAnalytics
|
|
if err := query.Order("last_accessed DESC").Find(&contentAnalytics).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch content analytics"})
|
|
return
|
|
}
|
|
|
|
// Group by content type
|
|
contentTypeStats := make(map[string]int)
|
|
categoryStats := make(map[string]int)
|
|
totalAccessCount := 0
|
|
totalTimeSpent := 0.0
|
|
|
|
for _, ca := range contentAnalytics {
|
|
contentTypeStats[ca.ContentType]++
|
|
if ca.Category != "" {
|
|
categoryStats[ca.Category]++
|
|
}
|
|
totalAccessCount += ca.AccessCount
|
|
totalTimeSpent += ca.TimeSpent
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"content_analytics": contentAnalytics,
|
|
"statistics": gin.H{
|
|
"total_access_count": totalAccessCount,
|
|
"total_time_spent": totalTimeSpent,
|
|
"content_types": contentTypeStats,
|
|
"categories": categoryStats,
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetGitHubAnalytics returns GitHub contribution analytics
|
|
func (h *AnalyticsHandler) GetGitHubAnalytics(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
days := 30
|
|
if d := c.Query("days"); d != "" {
|
|
if parsed, err := strconv.Atoi(d); err == nil && parsed > 0 {
|
|
days = parsed
|
|
}
|
|
}
|
|
|
|
startDate := time.Now().AddDate(0, 0, -days).Truncate(24 * time.Hour)
|
|
|
|
var githubAnalytics []models.GitHubAnalytics
|
|
if err := h.db.Where("user_id = ? AND date >= ?", userID, startDate).
|
|
Order("date DESC").
|
|
Find(&githubAnalytics).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch GitHub analytics"})
|
|
return
|
|
}
|
|
|
|
// Calculate summary statistics
|
|
totalCommits := 0
|
|
totalPullRequests := 0
|
|
totalIssuesOpened := 0
|
|
totalIssuesClosed := 0
|
|
totalReviews := 0
|
|
totalContributions := 0
|
|
languages := make(map[string]int)
|
|
repositories := make(map[string]bool)
|
|
|
|
for _, ga := range githubAnalytics {
|
|
totalCommits += ga.Commits
|
|
totalPullRequests += ga.PullRequests
|
|
totalIssuesOpened += ga.IssuesOpened
|
|
totalIssuesClosed += ga.IssuesClosed
|
|
totalReviews += ga.Reviews
|
|
totalContributions += ga.Contributions
|
|
|
|
for lang, count := range ga.Languages {
|
|
languages[lang] += count
|
|
}
|
|
|
|
for _, repo := range ga.Repositories {
|
|
repositories[repo] = true
|
|
}
|
|
}
|
|
|
|
// Convert repositories map to slice
|
|
repoList := make([]string, 0, len(repositories))
|
|
for repo := range repositories {
|
|
repoList = append(repoList, repo)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"github_analytics": githubAnalytics,
|
|
"summary": gin.H{
|
|
"total_commits": totalCommits,
|
|
"total_pull_requests": totalPullRequests,
|
|
"total_issues_opened": totalIssuesOpened,
|
|
"total_issues_closed": totalIssuesClosed,
|
|
"total_reviews": totalReviews,
|
|
"total_contributions": totalContributions,
|
|
"languages": languages,
|
|
"repositories": repoList,
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetGoals returns user goals with progress
|
|
func (h *AnalyticsHandler) GetGoals(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
status := c.Query("status")
|
|
category := c.Query("category")
|
|
|
|
query := h.db.Where("user_id = ?", userID).Preload("Milestones")
|
|
|
|
if status != "" {
|
|
query = query.Where("status = ?", status)
|
|
}
|
|
if category != "" {
|
|
query = query.Where("category = ?", category)
|
|
}
|
|
|
|
var goals []models.Goal
|
|
if err := query.Order("priority DESC, deadline ASC").Find(&goals).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch goals"})
|
|
return
|
|
}
|
|
|
|
// Calculate goal statistics
|
|
totalGoals := len(goals)
|
|
completedGoals := 0
|
|
activeGoals := 0
|
|
overdueGoals := 0
|
|
now := time.Now()
|
|
|
|
for _, goal := range goals {
|
|
if goal.IsCompleted {
|
|
completedGoals++
|
|
} else if goal.Status == "active" {
|
|
activeGoals++
|
|
if goal.Deadline.Before(now) {
|
|
overdueGoals++
|
|
}
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"goals": goals,
|
|
"statistics": gin.H{
|
|
"total_goals": totalGoals,
|
|
"completed_goals": completedGoals,
|
|
"active_goals": activeGoals,
|
|
"overdue_goals": overdueGoals,
|
|
},
|
|
})
|
|
}
|
|
|
|
// CreateGoal creates a new goal
|
|
func (h *AnalyticsHandler) CreateGoal(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
var req struct {
|
|
Title string `json:"title" binding:"required"`
|
|
Description string `json:"description"`
|
|
Category string `json:"category"`
|
|
TargetValue float64 `json:"target_value"`
|
|
CurrentValue float64 `json:"current_value"`
|
|
Unit string `json:"unit"`
|
|
Deadline time.Time `json:"deadline"`
|
|
Priority string `json:"priority"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
goal := models.Goal{
|
|
UserID: userID,
|
|
Title: req.Title,
|
|
Description: req.Description,
|
|
Category: req.Category,
|
|
TargetValue: req.TargetValue,
|
|
CurrentValue: req.CurrentValue,
|
|
Unit: req.Unit,
|
|
Deadline: req.Deadline,
|
|
Priority: req.Priority,
|
|
Status: "active",
|
|
Progress: (req.CurrentValue / req.TargetValue) * 100,
|
|
IsCompleted: req.CurrentValue >= req.TargetValue,
|
|
}
|
|
|
|
if goal.IsCompleted {
|
|
now := time.Now()
|
|
goal.CompletedAt = &now
|
|
goal.Status = "completed"
|
|
}
|
|
|
|
if err := h.db.Create(&goal).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create goal"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, goal)
|
|
}
|
|
|
|
// UpdateGoal updates an existing goal
|
|
func (h *AnalyticsHandler) UpdateGoal(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
goalID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"})
|
|
return
|
|
}
|
|
|
|
var goal models.Goal
|
|
if err := h.db.Where("id = ? AND user_id = ?", goalID, userID).First(&goal).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Title *string `json:"title,omitempty"`
|
|
Description *string `json:"description,omitempty"`
|
|
Category *string `json:"category,omitempty"`
|
|
TargetValue *float64 `json:"target_value,omitempty"`
|
|
CurrentValue *float64 `json:"current_value,omitempty"`
|
|
Unit *string `json:"unit,omitempty"`
|
|
Deadline *time.Time `json:"deadline,omitempty"`
|
|
Status *string `json:"status,omitempty"`
|
|
Priority *string `json:"priority,omitempty"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update fields if provided
|
|
if req.Title != nil {
|
|
goal.Title = *req.Title
|
|
}
|
|
if req.Description != nil {
|
|
goal.Description = *req.Description
|
|
}
|
|
if req.Category != nil {
|
|
goal.Category = *req.Category
|
|
}
|
|
if req.TargetValue != nil {
|
|
goal.TargetValue = *req.TargetValue
|
|
}
|
|
if req.CurrentValue != nil {
|
|
goal.CurrentValue = *req.CurrentValue
|
|
}
|
|
if req.Unit != nil {
|
|
goal.Unit = *req.Unit
|
|
}
|
|
if req.Deadline != nil {
|
|
goal.Deadline = *req.Deadline
|
|
}
|
|
if req.Status != nil {
|
|
goal.Status = *req.Status
|
|
}
|
|
if req.Priority != nil {
|
|
goal.Priority = *req.Priority
|
|
}
|
|
|
|
// Recalculate progress and completion status
|
|
goal.Progress = (goal.CurrentValue / goal.TargetValue) * 100
|
|
goal.IsCompleted = goal.CurrentValue >= goal.TargetValue
|
|
|
|
if goal.IsCompleted && goal.CompletedAt == nil {
|
|
now := time.Now()
|
|
goal.CompletedAt = &now
|
|
goal.Status = "completed"
|
|
} else if !goal.IsCompleted && goal.CompletedAt != nil {
|
|
goal.CompletedAt = nil
|
|
}
|
|
|
|
if err := h.db.Save(&goal).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update goal"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, goal)
|
|
}
|
|
|
|
// DeleteGoal deletes a goal
|
|
func (h *AnalyticsHandler) DeleteGoal(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
goalID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid goal ID"})
|
|
return
|
|
}
|
|
|
|
var goal models.Goal
|
|
if err := h.db.Where("id = ? AND user_id = ?", goalID, userID).First(&goal).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Goal not found"})
|
|
return
|
|
}
|
|
|
|
if err := h.db.Delete(&goal).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete goal"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Goal deleted successfully"})
|
|
}
|
|
|
|
// GenerateAnalyticsReport generates a comprehensive analytics report
|
|
func (h *AnalyticsHandler) GenerateAnalyticsReport(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
var req struct {
|
|
ReportType string `json:"report_type" binding:"required"`
|
|
StartDate time.Time `json:"start_date" binding:"required"`
|
|
EndDate time.Time `json:"end_date" binding:"required"`
|
|
Title string `json:"title"`
|
|
IsPublic bool `json:"is_public"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Generate report data
|
|
reportData := gin.H{}
|
|
|
|
// Get analytics for the period
|
|
var analytics []models.Analytics
|
|
h.db.Where("user_id = ? AND date BETWEEN ? AND ?", userID, req.StartDate, req.EndDate).
|
|
Find(&analytics)
|
|
|
|
// Get productivity metrics
|
|
var productivityMetrics []models.ProductivityMetrics
|
|
h.db.Where("user_id = ? AND start_date BETWEEN ? AND ?", userID, req.StartDate, req.EndDate).
|
|
Find(&productivityMetrics)
|
|
|
|
// Get learning analytics
|
|
var learningAnalytics []models.LearningAnalytics
|
|
h.db.Where("user_id = ?", userID).Preload("Course").Find(&learningAnalytics)
|
|
|
|
// Get GitHub analytics
|
|
var githubAnalytics []models.GitHubAnalytics
|
|
h.db.Where("user_id = ? AND date BETWEEN ? AND ?", userID, req.StartDate, req.EndDate).
|
|
Find(&githubAnalytics)
|
|
|
|
// Get goals
|
|
var goals []models.Goal
|
|
h.db.Where("user_id = ?", userID).Preload("Milestones").Find(&goals)
|
|
|
|
reportData["analytics"] = analytics
|
|
reportData["productivity_metrics"] = productivityMetrics
|
|
reportData["learning_analytics"] = learningAnalytics
|
|
reportData["github_analytics"] = githubAnalytics
|
|
reportData["goals"] = goals
|
|
|
|
// Generate insights and recommendations
|
|
insights := h.generateInsights(analytics, productivityMetrics, learningAnalytics, githubAnalytics, goals)
|
|
recommendations := h.generateRecommendations(analytics, productivityMetrics, learningAnalytics, githubAnalytics, goals)
|
|
|
|
report := models.AnalyticsReport{
|
|
UserID: userID,
|
|
ReportType: req.ReportType,
|
|
StartDate: req.StartDate,
|
|
EndDate: req.EndDate,
|
|
Title: req.Title,
|
|
Data: reportData,
|
|
Insights: insights,
|
|
Recommendations: recommendations,
|
|
IsPublic: req.IsPublic,
|
|
}
|
|
|
|
if err := h.db.Create(&report).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate report"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, report)
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
// GenerateDailyAnalytics generates daily analytics data for users
|
|
func (h *AnalyticsHandler) GenerateDailyAnalytics(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
|
|
// Get date from query params (default to today)
|
|
dateStr := c.DefaultQuery("date", time.Now().Format("2006-01-02"))
|
|
date, err := time.Parse("2006-01-02", dateStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid date format"})
|
|
return
|
|
}
|
|
|
|
startDate := date.Truncate(24 * time.Hour)
|
|
endDate := startDate.Add(24 * time.Hour)
|
|
|
|
// Get time entries for the day
|
|
var timeEntries []models.TimeEntry
|
|
h.db.Where("user_id = ? AND start_time BETWEEN ? AND ?", userID, startDate, endDate).
|
|
Preload("Task").
|
|
Preload("Bookmark").
|
|
Preload("Note").
|
|
Find(&timeEntries)
|
|
|
|
// Get tasks created/completed for the day
|
|
var tasks []models.Task
|
|
h.db.Where("user_id = ? AND (created_at BETWEEN ? AND ? OR updated_at BETWEEN ? AND ?)",
|
|
userID, startDate, endDate, startDate, endDate).
|
|
Find(&tasks)
|
|
|
|
// Get bookmarks created for the day
|
|
var bookmarksCreated int64
|
|
h.db.Model(&models.Bookmark{}).
|
|
Where("user_id = ? AND created_at BETWEEN ? AND ?", userID, startDate, endDate).
|
|
Count(&bookmarksCreated)
|
|
|
|
// Get notes created for the day
|
|
var notesCreated int64
|
|
h.db.Model(&models.Note{}).
|
|
Where("user_id = ? AND created_at BETWEEN ? AND ?", userID, startDate, endDate).
|
|
Count(¬esCreated)
|
|
|
|
// Get courses completed for the day
|
|
var coursesCompleted int64
|
|
h.db.Model(&models.Enrollment{}).
|
|
Where("user_id = ? AND completed_at BETWEEN ? AND ?", userID, startDate, endDate).
|
|
Count(&coursesCompleted)
|
|
|
|
// Get GitHub contributions for the day
|
|
var githubCommits int
|
|
h.db.Model(&models.GitHubAnalytics{}).
|
|
Where("user_id = ? AND date = ?", userID, startDate).
|
|
Select("COALESCE(commits, 0)").
|
|
Scan(&githubCommits)
|
|
|
|
// Calculate metrics
|
|
hoursTracked := 0.0
|
|
tasksCompleted := 0
|
|
studyStreak := 0
|
|
productivityScore := 0.0
|
|
|
|
for _, entry := range timeEntries {
|
|
duration := entry.GetDuration()
|
|
hoursTracked += float64(duration) / 3600.0
|
|
}
|
|
|
|
for _, task := range tasks {
|
|
if task.Status == "completed" && task.UpdatedAt.After(startDate) && task.UpdatedAt.Before(endDate) {
|
|
tasksCompleted++
|
|
}
|
|
}
|
|
|
|
// Calculate study streak (consecutive days with learning activity)
|
|
studyStreak = h.calculateStudyStreak(userID, date)
|
|
|
|
// Calculate productivity score
|
|
productivityScore = h.calculateProductivityScore(hoursTracked, tasksCompleted, int(bookmarksCreated), int(notesCreated))
|
|
|
|
// Create or update daily analytics
|
|
var existingAnalytics models.Analytics
|
|
err = h.db.Where("user_id = ? AND date = ?", userID, startDate).First(&existingAnalytics).Error
|
|
|
|
if err == nil {
|
|
// Update existing
|
|
existingAnalytics.HoursTracked = hoursTracked
|
|
existingAnalytics.TasksCompleted = tasksCompleted
|
|
existingAnalytics.BookmarksAdded = int(bookmarksCreated)
|
|
existingAnalytics.NotesCreated = int(notesCreated)
|
|
existingAnalytics.CoursesCompleted = int(coursesCompleted)
|
|
existingAnalytics.GitHubCommits = githubCommits
|
|
existingAnalytics.StudyStreak = studyStreak
|
|
existingAnalytics.ProductivityScore = productivityScore
|
|
h.db.Save(&existingAnalytics)
|
|
} else {
|
|
// Create new
|
|
analytics := models.Analytics{
|
|
UserID: userID,
|
|
Date: startDate,
|
|
HoursTracked: hoursTracked,
|
|
TasksCompleted: tasksCompleted,
|
|
BookmarksAdded: int(bookmarksCreated),
|
|
NotesCreated: int(notesCreated),
|
|
CoursesCompleted: int(coursesCompleted),
|
|
GitHubCommits: githubCommits,
|
|
StudyStreak: studyStreak,
|
|
ProductivityScore: productivityScore,
|
|
}
|
|
h.db.Create(&analytics)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "Daily analytics generated successfully",
|
|
"date": startDate.Format("2006-01-02"),
|
|
"metrics": gin.H{
|
|
"hours_tracked": hoursTracked,
|
|
"tasks_completed": tasksCompleted,
|
|
"bookmarks_added": bookmarksCreated,
|
|
"notes_created": notesCreated,
|
|
"courses_completed": coursesCompleted,
|
|
"github_commits": githubCommits,
|
|
"study_streak": studyStreak,
|
|
"productivity_score": productivityScore,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (h *AnalyticsHandler) calculateStudyStreak(userID uint, date time.Time) int {
|
|
// Calculate consecutive days with learning activity
|
|
streak := 0
|
|
currentDate := date.AddDate(0, 0, -1) // Start from yesterday
|
|
|
|
for i := 0; i < 365; i++ { // Check up to a year back
|
|
checkDate := currentDate.AddDate(0, 0, -i)
|
|
|
|
var count int64
|
|
h.db.Model(&models.LearningAnalytics{}).
|
|
Where("user_id = ? AND DATE(last_accessed) = ?", userID, checkDate.Format("2006-01-02")).
|
|
Count(&count)
|
|
|
|
if count > 0 {
|
|
streak++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return streak
|
|
}
|
|
|
|
func (h *AnalyticsHandler) calculateProductivityScore(hoursTracked float64, tasksCompleted, bookmarksAdded, notesCreated int) float64 {
|
|
// Simple productivity score calculation
|
|
// Base score from hours tracked (max 40 points)
|
|
hoursScore := 0.0
|
|
if hoursTracked > 0 {
|
|
hoursScore = math.Min(hoursTracked*2, 40) // 2 points per hour, max 40
|
|
}
|
|
|
|
// Tasks completed score (max 30 points)
|
|
tasksScore := math.Min(float64(tasksCompleted)*3, 30) // 3 points per task, max 30
|
|
|
|
// Content creation score (max 20 points)
|
|
contentScore := math.Min(float64(bookmarksAdded+notesCreated)*2, 20) // 2 points per item, max 20
|
|
|
|
// Bonus for well-rounded day (max 10 points)
|
|
bonus := 0.0
|
|
if hoursTracked > 4 && tasksCompleted > 0 && (bookmarksAdded > 0 || notesCreated > 0) {
|
|
bonus = 10
|
|
}
|
|
|
|
totalScore := hoursScore + tasksScore + contentScore + bonus
|
|
return math.Min(totalScore, 100) // Cap at 100
|
|
}
|
|
|
|
// generateProductivityMetrics generates productivity metrics for a given period
|
|
func (h *AnalyticsHandler) generateProductivityMetrics(userID uint, period string, startDate, endDate time.Time) {
|
|
// Get time entries for the period
|
|
var timeEntries []models.TimeEntry
|
|
h.db.Where("user_id = ? AND start_time BETWEEN ? AND ?", userID, startDate, endDate).
|
|
Preload("Task").
|
|
Preload("Bookmark").
|
|
Preload("Note").
|
|
Find(&timeEntries)
|
|
|
|
// Get tasks for the period
|
|
var tasks []models.Task
|
|
h.db.Where("user_id = ? AND created_at BETWEEN ? AND ?", userID, startDate, endDate).
|
|
Find(&tasks)
|
|
|
|
// Calculate metrics
|
|
totalHours := 0.0
|
|
billableHours := 0.0
|
|
nonBillableHours := 0.0
|
|
tasksCompleted := 0
|
|
hourlyProductivity := make(map[int]float64) // hour of day -> total hours
|
|
|
|
for _, entry := range timeEntries {
|
|
duration := entry.GetDuration()
|
|
hours := float64(duration) / 3600.0
|
|
totalHours += hours
|
|
|
|
if entry.Billable {
|
|
billableHours += hours
|
|
} else {
|
|
nonBillableHours += hours
|
|
}
|
|
|
|
// Track productivity by hour
|
|
hour := entry.StartTime.Hour()
|
|
hourlyProductivity[hour] += hours
|
|
}
|
|
|
|
// Count completed tasks
|
|
for _, task := range tasks {
|
|
if task.Status == "completed" {
|
|
tasksCompleted++
|
|
}
|
|
}
|
|
|
|
// Calculate average task time
|
|
averageTaskTime := 0.0
|
|
if tasksCompleted > 0 {
|
|
totalTaskTime := 0.0
|
|
taskCount := 0
|
|
for _, entry := range timeEntries {
|
|
if entry.TaskID != nil {
|
|
duration := entry.GetDuration()
|
|
totalTaskTime += float64(duration) / 3600.0
|
|
taskCount++
|
|
}
|
|
}
|
|
if taskCount > 0 {
|
|
averageTaskTime = totalTaskTime / float64(taskCount)
|
|
}
|
|
}
|
|
|
|
// Find peak productivity hour
|
|
peakProductivityHour := 9 // default to 9 AM
|
|
maxHours := 0.0
|
|
for hour, hours := range hourlyProductivity {
|
|
if hours > maxHours {
|
|
maxHours = hours
|
|
peakProductivityHour = hour
|
|
}
|
|
}
|
|
|
|
// Calculate focus score (based on uninterrupted time blocks)
|
|
focusScore := calculateFocusScore(timeEntries)
|
|
|
|
// Calculate efficiency score (tasks completed per hour)
|
|
efficiencyScore := 0.0
|
|
if totalHours > 0 {
|
|
efficiencyScore = float64(tasksCompleted) / totalHours * 100
|
|
}
|
|
|
|
// Create or update productivity metrics
|
|
var existingMetrics models.ProductivityMetrics
|
|
err := h.db.Where("user_id = ? AND period = ? AND start_date = ?", userID, period, startDate).
|
|
First(&existingMetrics).Error
|
|
|
|
if err == nil {
|
|
// Update existing
|
|
existingMetrics.TotalHours = totalHours
|
|
existingMetrics.BillableHours = billableHours
|
|
existingMetrics.NonBillableHours = nonBillableHours
|
|
existingMetrics.TasksCompleted = tasksCompleted
|
|
existingMetrics.AverageTaskTime = averageTaskTime
|
|
existingMetrics.PeakProductivityHour = peakProductivityHour
|
|
existingMetrics.FocusScore = focusScore
|
|
existingMetrics.EfficiencyScore = efficiencyScore
|
|
h.db.Save(&existingMetrics)
|
|
} else {
|
|
// Create new
|
|
metrics := models.ProductivityMetrics{
|
|
UserID: userID,
|
|
Period: period,
|
|
StartDate: startDate,
|
|
EndDate: endDate,
|
|
TotalHours: totalHours,
|
|
BillableHours: billableHours,
|
|
NonBillableHours: nonBillableHours,
|
|
TasksCompleted: tasksCompleted,
|
|
AverageTaskTime: averageTaskTime,
|
|
PeakProductivityHour: peakProductivityHour,
|
|
FocusScore: focusScore,
|
|
EfficiencyScore: efficiencyScore,
|
|
}
|
|
h.db.Create(&metrics)
|
|
}
|
|
}
|
|
|
|
func calculateFocusScore(timeEntries []models.TimeEntry) float64 {
|
|
if len(timeEntries) == 0 {
|
|
return 0.0
|
|
}
|
|
|
|
// Group entries by day and calculate focus score
|
|
// Focus score is based on the ratio of uninterrupted time blocks
|
|
totalUninterruptedTime := 0.0
|
|
totalTime := 0.0
|
|
|
|
// Sort time entries by start time
|
|
sortedEntries := make([]models.TimeEntry, len(timeEntries))
|
|
copy(sortedEntries, timeEntries)
|
|
|
|
// Simple implementation: check for gaps between entries
|
|
for i := 0; i < len(sortedEntries); i++ {
|
|
duration := float64(sortedEntries[i].GetDuration()) / 3600.0
|
|
totalTime += duration
|
|
|
|
// Check if this entry follows closely after the previous one
|
|
if i > 0 {
|
|
var previousEndTime time.Time
|
|
if sortedEntries[i-1].EndTime != nil {
|
|
previousEndTime = *sortedEntries[i-1].EndTime
|
|
} else {
|
|
previousEndTime = sortedEntries[i-1].StartTime.Add(time.Duration(sortedEntries[i-1].GetDuration()) * time.Second)
|
|
}
|
|
gap := sortedEntries[i].StartTime.Sub(previousEndTime)
|
|
if gap < 15*time.Minute { // Less than 15 minutes gap
|
|
totalUninterruptedTime += duration
|
|
}
|
|
} else {
|
|
totalUninterruptedTime += duration
|
|
}
|
|
}
|
|
|
|
if totalTime == 0 {
|
|
return 0.0
|
|
}
|
|
|
|
return (totalUninterruptedTime / totalTime) * 100
|
|
}
|
|
|
|
func (h *AnalyticsHandler) generateInsights(analytics []models.Analytics, productivityMetrics []models.ProductivityMetrics, learningAnalytics []models.LearningAnalytics, githubAnalytics []models.GitHubAnalytics, goals []models.Goal) []string {
|
|
insights := []string{}
|
|
|
|
// Generate insights based on data
|
|
if len(analytics) > 0 {
|
|
totalHours := 0.0
|
|
for _, a := range analytics {
|
|
totalHours += a.HoursTracked
|
|
}
|
|
if totalHours > 100 {
|
|
insights = append(insights, "Great job! You've tracked over 100 hours of productive work.")
|
|
}
|
|
}
|
|
|
|
if len(goals) > 0 {
|
|
completed := 0
|
|
for _, goal := range goals {
|
|
if goal.IsCompleted {
|
|
completed++
|
|
}
|
|
}
|
|
if completed > 0 {
|
|
insights = append(insights, "You've completed "+strconv.Itoa(completed)+" goals. Keep up the momentum!")
|
|
}
|
|
}
|
|
|
|
return insights
|
|
}
|
|
|
|
func (h *AnalyticsHandler) generateRecommendations(analytics []models.Analytics, productivityMetrics []models.ProductivityMetrics, learningAnalytics []models.LearningAnalytics, githubAnalytics []models.GitHubAnalytics, goals []models.Goal) []string {
|
|
recommendations := []string{}
|
|
|
|
// Generate recommendations based on data
|
|
if len(analytics) > 0 {
|
|
totalHours := 0.0
|
|
for _, a := range analytics {
|
|
totalHours += a.HoursTracked
|
|
}
|
|
if totalHours < 20 {
|
|
recommendations = append(recommendations, "Consider tracking more time to get better insights into your productivity patterns.")
|
|
}
|
|
}
|
|
|
|
if len(goals) > 0 {
|
|
overdue := 0
|
|
now := time.Now()
|
|
for _, goal := range goals {
|
|
if goal.Status == "active" && goal.Deadline.Before(now) {
|
|
overdue++
|
|
}
|
|
}
|
|
if overdue > 0 {
|
|
recommendations = append(recommendations, "You have "+strconv.Itoa(overdue)+" overdue goals. Consider updating deadlines or priorities.")
|
|
}
|
|
}
|
|
|
|
return recommendations
|
|
}
|