Files
training-tracker/backend/internal/storage/sessions.go
2026-01-19 19:49:59 +01:00

240 lines
6.9 KiB
Go

package storage
import (
"context"
"database/sql"
"fmt"
"training-tracker/internal/models"
)
type SessionRepository struct {
db *DB
}
func NewSessionRepository(db *DB) *SessionRepository {
return &SessionRepository{db: db}
}
func (r *SessionRepository) List(ctx context.Context) ([]models.Session, error) {
query := `
SELECT s.id, s.plan_id, s.date, s.notes, s.created_at,
p.id, p.name, p.description, p.created_at
FROM sessions s
LEFT JOIN training_plans p ON s.plan_id = p.id
ORDER BY s.date DESC`
rows, err := r.db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to query sessions: %w", err)
}
defer rows.Close()
var sessions []models.Session
for rows.Next() {
var s models.Session
var planID sql.NullInt64
var notes sql.NullString
var planIDVal, planName, planDesc sql.NullString
var planCreatedAt sql.NullTime
if err := rows.Scan(
&s.ID, &planID, &s.Date, &notes, &s.CreatedAt,
&planIDVal, &planName, &planDesc, &planCreatedAt,
); err != nil {
return nil, fmt.Errorf("failed to scan session: %w", err)
}
s.Notes = notes.String
if planID.Valid {
pid := planID.Int64
s.PlanID = &pid
s.Plan = &models.TrainingPlan{
ID: pid,
Name: planName.String,
Description: planDesc.String,
CreatedAt: planCreatedAt.Time,
}
}
sessions = append(sessions, s)
}
return sessions, nil
}
func (r *SessionRepository) GetByID(ctx context.Context, id int64) (*models.Session, error) {
query := `
SELECT s.id, s.plan_id, s.date, s.notes, s.created_at,
p.id, p.name, p.description, p.created_at
FROM sessions s
LEFT JOIN training_plans p ON s.plan_id = p.id
WHERE s.id = $1`
var s models.Session
var planID sql.NullInt64
var notes sql.NullString
var planIDVal, planName, planDesc sql.NullString
var planCreatedAt sql.NullTime
err := r.db.QueryRowContext(ctx, query, id).Scan(
&s.ID, &planID, &s.Date, &notes, &s.CreatedAt,
&planIDVal, &planName, &planDesc, &planCreatedAt,
)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to get session: %w", err)
}
s.Notes = notes.String
if planID.Valid {
pid := planID.Int64
s.PlanID = &pid
s.Plan = &models.TrainingPlan{
ID: pid,
Name: planName.String,
Description: planDesc.String,
CreatedAt: planCreatedAt.Time,
}
}
entriesQuery := `
SELECT se.id, se.session_id, se.exercise_id, se.weight, se.reps, se.sets_completed,
se.duration, se.distance, se.heart_rate, se.notes, se.created_at,
e.id, e.name, e.type, e.muscle_group, e.description, e.created_at
FROM session_entries se
JOIN exercises e ON se.exercise_id = e.id
WHERE se.session_id = $1
ORDER BY se.created_at`
rows, err := r.db.QueryContext(ctx, entriesQuery, id)
if err != nil {
return nil, fmt.Errorf("failed to query session entries: %w", err)
}
defer rows.Close()
for rows.Next() {
var se models.SessionEntry
var weight, distance sql.NullFloat64
var reps, setsCompleted, duration, heartRate sql.NullInt64
var entryNotes, muscleGroup, exerciseDesc sql.NullString
var exercise models.Exercise
if err := rows.Scan(
&se.ID, &se.SessionID, &se.ExerciseID, &weight, &reps, &setsCompleted,
&duration, &distance, &heartRate, &entryNotes, &se.CreatedAt,
&exercise.ID, &exercise.Name, &exercise.Type, &muscleGroup, &exerciseDesc, &exercise.CreatedAt,
); err != nil {
return nil, fmt.Errorf("failed to scan session entry: %w", err)
}
se.Weight = weight.Float64
se.Reps = int(reps.Int64)
se.SetsCompleted = int(setsCompleted.Int64)
se.Duration = int(duration.Int64)
se.Distance = distance.Float64
se.HeartRate = int(heartRate.Int64)
se.Notes = entryNotes.String
exercise.MuscleGroup = muscleGroup.String
exercise.Description = exerciseDesc.String
se.Exercise = &exercise
s.Entries = append(s.Entries, se)
}
return &s, nil
}
func (r *SessionRepository) Create(ctx context.Context, req *models.CreateSessionRequest) (*models.Session, error) {
query := `INSERT INTO sessions (plan_id, date, notes)
VALUES ($1, $2, $3)
RETURNING id, plan_id, date, notes, created_at`
var s models.Session
var planID sql.NullInt64
var notes sql.NullString
var reqPlanID sql.NullInt64
if req.PlanID != nil {
reqPlanID = sql.NullInt64{Int64: *req.PlanID, Valid: true}
}
err := r.db.QueryRowContext(ctx, query, reqPlanID, req.Date, nullString(req.Notes)).
Scan(&s.ID, &planID, &s.Date, &notes, &s.CreatedAt)
if err != nil {
return nil, fmt.Errorf("failed to create session: %w", err)
}
s.Notes = notes.String
if planID.Valid {
pid := planID.Int64
s.PlanID = &pid
}
return &s, nil
}
func (r *SessionRepository) Update(ctx context.Context, id int64, req *models.CreateSessionRequest) (*models.Session, error) {
var reqPlanID sql.NullInt64
if req.PlanID != nil {
reqPlanID = sql.NullInt64{Int64: *req.PlanID, Valid: true}
}
query := `UPDATE sessions SET plan_id = $1, date = $2, notes = $3 WHERE id = $4`
result, err := r.db.ExecContext(ctx, query, reqPlanID, req.Date, nullString(req.Notes), id)
if err != nil {
return nil, fmt.Errorf("failed to update session: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return nil, nil
}
return r.GetByID(ctx, id)
}
func (r *SessionRepository) Delete(ctx context.Context, id int64) error {
query := `DELETE FROM sessions WHERE id = $1`
result, err := r.db.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("failed to delete session: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return sql.ErrNoRows
}
return nil
}
func (r *SessionRepository) AddEntry(ctx context.Context, sessionID int64, req *models.CreateSessionEntryRequest) (*models.SessionEntry, error) {
query := `INSERT INTO session_entries (session_id, exercise_id, weight, reps, sets_completed, duration, distance, heart_rate, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id, session_id, exercise_id, weight, reps, sets_completed, duration, distance, heart_rate, notes, created_at`
var se models.SessionEntry
var weight, distance sql.NullFloat64
var reps, setsCompleted, duration, heartRate sql.NullInt64
var notes sql.NullString
err := r.db.QueryRowContext(ctx, query, sessionID, req.ExerciseID,
nullFloat(req.Weight), nullInt(req.Reps), nullInt(req.SetsCompleted),
nullInt(req.Duration), nullFloat(req.Distance), nullInt(req.HeartRate),
nullString(req.Notes)).
Scan(&se.ID, &se.SessionID, &se.ExerciseID, &weight, &reps, &setsCompleted, &duration, &distance, &heartRate, &notes, &se.CreatedAt)
if err != nil {
return nil, fmt.Errorf("failed to create session entry: %w", err)
}
se.Weight = weight.Float64
se.Reps = int(reps.Int64)
se.SetsCompleted = int(setsCompleted.Int64)
se.Duration = int(duration.Int64)
se.Distance = distance.Float64
se.HeartRate = int(heartRate.Int64)
se.Notes = notes.String
return &se, nil
}
func nullFloat(f float64) sql.NullFloat64 {
if f == 0 {
return sql.NullFloat64{}
}
return sql.NullFloat64{Float64: f, Valid: true}
}