240 lines
6.9 KiB
Go
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, ¬es, &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, ¬es, &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, ¬es, &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, ¬es, &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}
|
|
}
|