package services import ( "database/sql" "encoding/json" "time" "github.com/rede5/gohorsejobs/backend/internal/models" ) type ActivityLogService struct { db *sql.DB } func NewActivityLogService(db *sql.DB) *ActivityLogService { return &ActivityLogService{db: db} } // Log creates a new activity log entry func (s *ActivityLogService) Log(userID *int, tenantID *string, action string, resourceType, resourceID *string, description *string, metadata map[string]interface{}, ipAddress, userAgent *string) error { var metadataJSON []byte if metadata != nil { metadataJSON, _ = json.Marshal(metadata) } query := ` INSERT INTO activity_logs (user_id, tenant_id, action, resource_type, resource_id, description, metadata, ip_address, user_agent) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ` _, err := s.db.Exec(query, userID, tenantID, action, resourceType, resourceID, description, metadataJSON, ipAddress, userAgent) return err } // List lists activity logs with filters func (s *ActivityLogService) List(filter models.ActivityLogFilter) ([]models.ActivityLog, error) { query := ` SELECT al.id, al.user_id, al.tenant_id, al.action, al.resource_type, al.resource_id, al.description, al.metadata, al.ip_address, al.user_agent, al.created_at, u.full_name as user_name FROM activity_logs al LEFT JOIN users u ON al.user_id = u.id WHERE ($1::int IS NULL OR al.user_id = $1) AND ($2::varchar IS NULL OR al.tenant_id = $2) AND ($3::varchar IS NULL OR al.action = $3) AND ($4::varchar IS NULL OR al.resource_type = $4) AND ($5::timestamp IS NULL OR al.created_at >= $5) AND ($6::timestamp IS NULL OR al.created_at <= $6) ORDER BY al.created_at DESC LIMIT $7 OFFSET $8 ` limit := filter.Limit if limit == 0 { limit = 50 } rows, err := s.db.Query(query, filter.UserID, filter.TenantID, filter.Action, filter.ResourceType, filter.StartDate, filter.EndDate, limit, filter.Offset, ) if err != nil { return nil, err } defer rows.Close() var logs []models.ActivityLog for rows.Next() { var log models.ActivityLog err := rows.Scan( &log.ID, &log.UserID, &log.TenantID, &log.Action, &log.ResourceType, &log.ResourceID, &log.Description, &log.Metadata, &log.IPAddress, &log.UserAgent, &log.CreatedAt, &log.UserName, ) if err != nil { return nil, err } logs = append(logs, log) } return logs, nil } // GetStats gets activity log statistics func (s *ActivityLogService) GetStats() (*models.ActivityLogStats, error) { stats := &models.ActivityLogStats{} now := time.Now() // Counts countQuery := ` SELECT COUNT(*) FILTER (WHERE created_at >= $1) as today, COUNT(*) FILTER (WHERE created_at >= $2) as this_week, COUNT(*) FILTER (WHERE created_at >= $3) as this_month FROM activity_logs ` startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) startOfWeek := startOfDay.AddDate(0, 0, -int(now.Weekday())) startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) err := s.db.QueryRow(countQuery, startOfDay, startOfWeek, startOfMonth). Scan(&stats.TotalToday, &stats.TotalThisWeek, &stats.TotalThisMonth) if err != nil { return nil, err } // Top actions topActionsQuery := ` SELECT action, COUNT(*) as count FROM activity_logs WHERE created_at >= $1 GROUP BY action ORDER BY count DESC LIMIT 10 ` rows, err := s.db.Query(topActionsQuery, startOfWeek) if err == nil { defer rows.Close() for rows.Next() { var ac models.ActionCount if err := rows.Scan(&ac.Action, &ac.Count); err == nil { stats.TopActions = append(stats.TopActions, ac) } } } // Recent activity (last 20) recentLogs, _ := s.List(models.ActivityLogFilter{Limit: 20}) stats.RecentActivity = recentLogs return stats, nil }