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} }