package repoService import ( "fmt" "slices" "git.ma-al.com/goc_marek/timetracker/app/db" "git.ma-al.com/goc_marek/timetracker/app/model" "git.ma-al.com/goc_marek/timetracker/app/utils/pagination" "git.ma-al.com/goc_marek/timetracker/app/view" "gorm.io/gorm" ) // type type RepoService struct { db *gorm.DB } func New() *RepoService { return &RepoService{ db: db.Get(), } } func (s *RepoService) GetRepositoriesForUser(userID uint) ([]uint, error) { var repoIDs []uint err := s.db. Table("customer_repo_accesses"). Where("user_id = ?", userID). Pluck("repo_id", &repoIDs).Error if err != nil { return nil, fmt.Errorf("database error: %w", err) } return repoIDs, nil } func (s *RepoService) UserHasAccessToRepo(userID uint, repoID uint) (bool, error) { var repositories []uint var err error if repositories, err = s.GetRepositoriesForUser(userID); err != nil { return false, err } if !slices.Contains(repositories, repoID) { return false, view.ErrInvalidRepoID } return true, nil } // Extract all repositories assigned to user with specific id func (s *RepoService) GetYearsForUser(userID uint, repoID uint) ([]uint, error) { if ok, err := s.UserHasAccessToRepo(userID, repoID); !ok { return nil, err } years, err := s.GetYears(repoID) if err != nil { return nil, fmt.Errorf("database error: %w", err) } return years, nil } func (s *RepoService) GetYears(repo uint) ([]uint, error) { var years []uint query := ` WITH bounds AS ( SELECT MIN(to_timestamp(tt.created_unix)) AS min_ts, MAX(to_timestamp(tt.created_unix)) AS max_ts FROM tracked_time tt JOIN issue i ON i.id = tt.issue_id WHERE i.repo_id = ? AND tt.deleted = false ) SELECT EXTRACT(YEAR FROM y.year_start)::int AS year FROM bounds CROSS JOIN LATERAL generate_series( date_trunc('year', min_ts), date_trunc('year', max_ts), interval '1 year' ) AS y(year_start) ORDER BY year ` err := db.Get().Raw(query, repo).Find(&years).Error if err != nil { return nil, err } return years, nil } // Extract all repositories assigned to user with specific id func (s *RepoService) GetQuartersForUser(userID uint, repoID uint, year uint) ([]model.QuarterData, error) { if ok, err := s.UserHasAccessToRepo(userID, repoID); !ok { return nil, err } response, err := s.GetQuarters(repoID, year) if err != nil { return nil, fmt.Errorf("database error: %w", err) } return response, nil } func (s *RepoService) GetQuarters(repo uint, year uint) ([]model.QuarterData, error) { var quarters []model.QuarterData query := ` WITH quarters AS ( SELECT make_date(?::int, 1, 1) + (q * interval '3 months') AS quarter_start, q + 1 AS quarter FROM generate_series(0, 3) AS q ), data AS ( SELECT EXTRACT(QUARTER FROM to_timestamp(tt.created_unix)) AS quarter, SUM(tt.time) / 3600 AS time FROM tracked_time tt JOIN issue i ON i.id = tt.issue_id JOIN repository r ON i.repo_id = r.id WHERE EXTRACT(YEAR FROM to_timestamp(tt.created_unix)) = ? AND r.id = ? AND tt.deleted = false GROUP BY EXTRACT(QUARTER FROM to_timestamp(tt.created_unix)) ) SELECT COALESCE(d.time, 0) AS time, CONCAT(EXTRACT(YEAR FROM q.quarter_start)::int, '_Q', q.quarter) AS quarter FROM quarters q LEFT JOIN data d ON d.quarter = q.quarter ORDER BY q.quarter ` err := db.Get(). Raw(query, year, year, repo). Find(&quarters). Error if err != nil { fmt.Printf("err: %v\n", err) return nil, err } return quarters, nil } func (s *RepoService) GetTotalTimeForQuarter(repo uint, year uint, quarter uint) (float64, error) { var total float64 query := ` SELECT COALESCE(SUM(tt.time) / 3600, 0) AS total_time FROM tracked_time tt JOIN issue i ON i.id = tt.issue_id WHERE i.repo_id = ? AND EXTRACT(YEAR FROM to_timestamp(tt.created_unix)) = ? AND EXTRACT(QUARTER FROM to_timestamp(tt.created_unix)) = ? AND tt.deleted = false ` err := db.Get().Raw(query, repo, year, quarter).Row().Scan(&total) if err != nil { return 0, err } return total, nil } func (s *RepoService) GetTimeTracked(repo uint, year uint, quarter uint, step string) ([]model.DayData, error) { var days []model.DayData // Calculate quarter start and end dates quarterStartMonth := (quarter-1)*3 + 1 quarterStart := fmt.Sprintf("%d-%02d-01", year, quarterStartMonth) var quarterEnd string switch quarter { case 1: quarterEnd = fmt.Sprintf("%d-03-31", year) case 2: quarterEnd = fmt.Sprintf("%d-06-30", year) case 3: quarterEnd = fmt.Sprintf("%d-09-30", year) default: quarterEnd = fmt.Sprintf("%d-12-31", year) } var bucketExpr string var seriesInterval string var seriesStart string var seriesEnd string switch step { case "day": bucketExpr = "DATE(to_timestamp(tt.created_unix))" seriesInterval = "1 day" seriesStart = "p.start_date" seriesEnd = "p.end_date" case "week": bucketExpr = ` (p.start_date + ((DATE(to_timestamp(tt.created_unix)) - p.start_date) / 7) * 7 )::date` seriesInterval = "7 days" seriesStart = "p.start_date" seriesEnd = "p.end_date" case "month": bucketExpr = "date_trunc('month', to_timestamp(tt.created_unix))::date" seriesInterval = "1 month" seriesStart = "date_trunc('month', p.start_date)" seriesEnd = "date_trunc('month', p.end_date)" } query := fmt.Sprintf(` WITH params AS ( SELECT ?::date AS start_date, ?::date AS end_date ), date_range AS ( SELECT generate_series( %s, %s, interval '%s' )::date AS date FROM params p ), data AS ( SELECT %s AS date, SUM(tt.time) / 3600 AS time FROM tracked_time tt JOIN issue i ON i.id = tt.issue_id CROSS JOIN params p WHERE i.repo_id = ? AND to_timestamp(tt.created_unix) >= p.start_date AND to_timestamp(tt.created_unix) < p.end_date + interval '1 day' AND tt.deleted = false GROUP BY 1 ) SELECT TO_CHAR(dr.date, 'YYYY-MM-DD') AS date, COALESCE(d.time, 0) AS time FROM date_range dr LEFT JOIN data d ON d.date = dr.date ORDER BY dr.date `, seriesStart, seriesEnd, seriesInterval, bucketExpr) err := db.Get(). Raw(query, quarterStart, quarterEnd, repo). Scan(&days).Error if err != nil { return nil, err } return days, nil } func (s *RepoService) GetRepoData(repoIds []uint) ([]model.Repository, error) { var repos []model.Repository err := db.Get().Model(model.Repository{}).Where("id = ?", repoIds).Find(&repos).Error if err != nil { return nil, err } return repos, nil } func (s *RepoService) GetIssuesForUser( userID uint, repoID uint, year uint, quarter uint, p pagination.Paging, ) (*pagination.Found[view.IssueTimeSummary], error) { if ok, err := s.UserHasAccessToRepo(userID, repoID); !ok { return nil, err } return s.GetIssues(repoID, year, quarter, p) } func (s *RepoService) GetIssues( repoId uint, year uint, quarter uint, p pagination.Paging, ) (*pagination.Found[view.IssueTimeSummary], error) { query := db.Get().Debug(). Table("issue i"). Select(` i.id AS issue_id, i.name AS issue_name, u.id AS user_id, upper( regexp_replace( regexp_replace(u.full_name, '(\y\w)\w*', '\1', 'g'), '(\w)', '\1.', 'g' ) ) AS initials, to_timestamp(tt.created_unix)::date AS created_date, ROUND(SUM(tt.time) / 3600.0, 2) AS total_hours_spent `). Joins(`JOIN tracked_time tt ON tt.issue_id = i.id`). Joins(`JOIN "user" u ON u.id = tt.user_id`). Where("i.repo_id = ?", repoId). Where(` EXTRACT(YEAR FROM to_timestamp(tt.created_unix)) = ? AND EXTRACT(QUARTER FROM to_timestamp(tt.created_unix)) = ? `, year, quarter). Group(` i.id, i.name, u.id, u.full_name, created_date `). Order("created_date") result, err := pagination.Paginate[view.IssueTimeSummary](p, query) if err != nil { return nil, err } return &result, nil }