package storage import ( "context" "database/sql" "fmt" "training-tracker/internal/models" ) type PlanRepository struct { db *DB } func NewPlanRepository(db *DB) *PlanRepository { return &PlanRepository{db: db} } func (r *PlanRepository) List(ctx context.Context) ([]models.TrainingPlan, error) { query := `SELECT id, name, description, created_at FROM training_plans ORDER BY name` rows, err := r.db.QueryContext(ctx, query) if err != nil { return nil, fmt.Errorf("failed to query plans: %w", err) } defer rows.Close() var plans []models.TrainingPlan for rows.Next() { var p models.TrainingPlan var description sql.NullString if err := rows.Scan(&p.ID, &p.Name, &description, &p.CreatedAt); err != nil { return nil, fmt.Errorf("failed to scan plan: %w", err) } p.Description = description.String plans = append(plans, p) } return plans, nil } func (r *PlanRepository) GetByID(ctx context.Context, id int64) (*models.TrainingPlan, error) { query := `SELECT id, name, description, created_at FROM training_plans WHERE id = $1` var p models.TrainingPlan var description sql.NullString err := r.db.QueryRowContext(ctx, query, id).Scan(&p.ID, &p.Name, &description, &p.CreatedAt) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("failed to get plan: %w", err) } p.Description = description.String exerciseQuery := ` SELECT pe.id, pe.plan_id, pe.exercise_id, pe.sets, pe.reps, pe.duration, pe.sort_order, e.id, e.name, e.type, e.muscle_group, e.description, e.created_at FROM plan_exercises pe JOIN exercises e ON pe.exercise_id = e.id WHERE pe.plan_id = $1 ORDER BY pe.sort_order` rows, err := r.db.QueryContext(ctx, exerciseQuery, id) if err != nil { return nil, fmt.Errorf("failed to query plan exercises: %w", err) } defer rows.Close() for rows.Next() { var pe models.PlanExercise var sets, reps, duration sql.NullInt64 var muscleGroup, exerciseDesc sql.NullString if err := rows.Scan( &pe.ID, &pe.PlanID, &pe.ExerciseID, &sets, &reps, &duration, &pe.Order, &pe.Exercise.ID, &pe.Exercise.Name, &pe.Exercise.Type, &muscleGroup, &exerciseDesc, &pe.Exercise.CreatedAt, ); err != nil { return nil, fmt.Errorf("failed to scan plan exercise: %w", err) } pe.Sets = int(sets.Int64) pe.Reps = int(reps.Int64) pe.Duration = int(duration.Int64) pe.Exercise.MuscleGroup = muscleGroup.String pe.Exercise.Description = exerciseDesc.String p.Exercises = append(p.Exercises, pe) } return &p, nil } func (r *PlanRepository) Create(ctx context.Context, req *models.CreatePlanRequest) (*models.TrainingPlan, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return nil, fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() query := `INSERT INTO training_plans (name, description) VALUES ($1, $2) RETURNING id, name, description, created_at` var p models.TrainingPlan var description sql.NullString err = tx.QueryRowContext(ctx, query, req.Name, nullString(req.Description)). Scan(&p.ID, &p.Name, &description, &p.CreatedAt) if err != nil { return nil, fmt.Errorf("failed to create plan: %w", err) } p.Description = description.String for _, pe := range req.Exercises { exerciseQuery := `INSERT INTO plan_exercises (plan_id, exercise_id, sets, reps, duration, sort_order) VALUES ($1, $2, $3, $4, $5, $6)` _, err = tx.ExecContext(ctx, exerciseQuery, p.ID, pe.ExerciseID, nullInt(pe.Sets), nullInt(pe.Reps), nullInt(pe.Duration), pe.Order) if err != nil { return nil, fmt.Errorf("failed to create plan exercise: %w", err) } } if err := tx.Commit(); err != nil { return nil, fmt.Errorf("failed to commit transaction: %w", err) } return r.GetByID(ctx, p.ID) } func (r *PlanRepository) Update(ctx context.Context, id int64, req *models.CreatePlanRequest) (*models.TrainingPlan, error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return nil, fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() query := `UPDATE training_plans SET name = $1, description = $2 WHERE id = $3` result, err := tx.ExecContext(ctx, query, req.Name, nullString(req.Description), id) if err != nil { return nil, fmt.Errorf("failed to update plan: %w", err) } rows, _ := result.RowsAffected() if rows == 0 { return nil, nil } _, err = tx.ExecContext(ctx, `DELETE FROM plan_exercises WHERE plan_id = $1`, id) if err != nil { return nil, fmt.Errorf("failed to delete plan exercises: %w", err) } for _, pe := range req.Exercises { exerciseQuery := `INSERT INTO plan_exercises (plan_id, exercise_id, sets, reps, duration, sort_order) VALUES ($1, $2, $3, $4, $5, $6)` _, err = tx.ExecContext(ctx, exerciseQuery, id, pe.ExerciseID, nullInt(pe.Sets), nullInt(pe.Reps), nullInt(pe.Duration), pe.Order) if err != nil { return nil, fmt.Errorf("failed to create plan exercise: %w", err) } } if err := tx.Commit(); err != nil { return nil, fmt.Errorf("failed to commit transaction: %w", err) } return r.GetByID(ctx, id) } func (r *PlanRepository) Delete(ctx context.Context, id int64) error { query := `DELETE FROM training_plans WHERE id = $1` result, err := r.db.ExecContext(ctx, query, id) if err != nil { return fmt.Errorf("failed to delete plan: %w", err) } rows, _ := result.RowsAffected() if rows == 0 { return sql.ErrNoRows } return nil } func nullInt(i int) sql.NullInt64 { if i == 0 { return sql.NullInt64{} } return sql.NullInt64{Int64: int64(i), Valid: true} }