package store import ( "database/sql" "fmt" "krafttrainer/internal/model" "strings" ) // CreateSession startet eine neue Trainingseinheit für einen Nutzer. func (s *Store) CreateSession(userID, setID int64) (*model.Session, error) { var setName string err := s.db.QueryRow(`SELECT name FROM training_sets WHERE id = ? AND user_id = ? AND deleted_at IS NULL`, setID, userID).Scan(&setName) if err == sql.ErrNoRows { return nil, fmt.Errorf("Set %d existiert nicht", setID) } if err != nil { return nil, fmt.Errorf("Set prüfen: %w", err) } result, err := s.db.Exec(`INSERT INTO sessions (set_id, user_id) VALUES (?, ?)`, setID, userID) if err != nil { return nil, fmt.Errorf("Session erstellen: %w", err) } id, _ := result.LastInsertId() return s.GetSession(id) } // GetSession gibt eine Session mit allen Logs zurück (intern, ohne User-Scope). func (s *Store) GetSession(id int64) (*model.Session, error) { var sess model.Session err := s.db.QueryRow(` SELECT s.id, s.set_id, ts.name, s.started_at, s.ended_at, s.note FROM sessions s JOIN training_sets ts ON ts.id = s.set_id WHERE s.id = ?`, id, ).Scan(&sess.ID, &sess.SetID, &sess.SetName, &sess.StartedAt, &sess.EndedAt, &sess.Note) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("Session abfragen: %w", err) } logs, err := s.getSessionLogs(id) if err != nil { return nil, err } sess.Logs = logs return &sess, nil } // EndSession beendet eine Session eines Nutzers. func (s *Store) EndSession(id, userID int64, note string) (*model.Session, error) { result, err := s.db.Exec(` UPDATE sessions SET ended_at = CURRENT_TIMESTAMP, note = ? WHERE id = ? AND user_id = ? AND ended_at IS NULL`, note, id, userID, ) if err != nil { return nil, fmt.Errorf("Session beenden: %w", err) } rows, _ := result.RowsAffected() if rows == 0 { return nil, nil } return s.GetSession(id) } // ListSessions gibt paginierte Sessions eines Nutzers zurück (neueste zuerst). func (s *Store) ListSessions(userID int64, limit, offset int) ([]model.Session, error) { rows, err := s.db.Query(` SELECT s.id, s.set_id, ts.name, s.started_at, s.ended_at, s.note FROM sessions s JOIN training_sets ts ON ts.id = s.set_id WHERE s.user_id = ? ORDER BY s.started_at DESC LIMIT ? OFFSET ?`, userID, limit, offset, ) if err != nil { return nil, fmt.Errorf("Sessions abfragen: %w", err) } defer rows.Close() var sessions []model.Session for rows.Next() { var sess model.Session if err := rows.Scan(&sess.ID, &sess.SetID, &sess.SetName, &sess.StartedAt, &sess.EndedAt, &sess.Note); err != nil { return nil, fmt.Errorf("Session scannen: %w", err) } sessions = append(sessions, sess) } if sessions == nil { sessions = []model.Session{} } return sessions, rows.Err() } // CreateLog fügt einen Satz zu einer offenen Session hinzu. func (s *Store) CreateLog(sessionID int64, req *model.CreateLogRequest) (*model.SessionLog, error) { if err := s.checkSessionOpen(sessionID); err != nil { return nil, err } var exerciseName string err := s.db.QueryRow(`SELECT name FROM exercises WHERE id = ? AND deleted_at IS NULL`, req.ExerciseID).Scan(&exerciseName) if err == sql.ErrNoRows { return nil, fmt.Errorf("Übung %d existiert nicht", req.ExerciseID) } if err != nil { return nil, fmt.Errorf("Übung abfragen: %w", err) } result, err := s.db.Exec(` INSERT INTO session_logs (session_id, exercise_id, exercise_name, set_number, weight_kg, reps, note) VALUES (?, ?, ?, ?, ?, ?, ?)`, sessionID, req.ExerciseID, exerciseName, req.SetNumber, req.WeightKg, req.Reps, req.Note, ) if err != nil { if strings.Contains(err.Error(), "UNIQUE constraint") { return nil, fmt.Errorf("UNIQUE_VIOLATION: Satz %d für diese Übung existiert bereits", req.SetNumber) } return nil, fmt.Errorf("Log erstellen: %w", err) } id, _ := result.LastInsertId() return s.getLog(id) } // UpdateLog korrigiert einen Satz in einer offenen Session. func (s *Store) UpdateLog(sessionID, logID int64, req *model.UpdateLogRequest) (*model.SessionLog, error) { if err := s.checkSessionOpen(sessionID); err != nil { return nil, err } var exists bool err := s.db.QueryRow(`SELECT EXISTS(SELECT 1 FROM session_logs WHERE id = ? AND session_id = ?)`, logID, sessionID).Scan(&exists) if err != nil { return nil, fmt.Errorf("Log prüfen: %w", err) } if !exists { return nil, nil } updates := []string{} args := []any{} if req.WeightKg != nil { updates = append(updates, "weight_kg = ?") args = append(args, *req.WeightKg) } if req.Reps != nil { updates = append(updates, "reps = ?") args = append(args, *req.Reps) } if req.Note != nil { updates = append(updates, "note = ?") args = append(args, *req.Note) } if len(updates) == 0 { return s.getLog(logID) } args = append(args, logID) _, err = s.db.Exec( fmt.Sprintf("UPDATE session_logs SET %s WHERE id = ?", strings.Join(updates, ", ")), args..., ) if err != nil { return nil, fmt.Errorf("Log aktualisieren: %w", err) } return s.getLog(logID) } // DeleteLog löscht einen Satz aus einer offenen Session. func (s *Store) DeleteLog(sessionID, logID int64) error { if err := s.checkSessionOpen(sessionID); err != nil { return err } result, err := s.db.Exec(`DELETE FROM session_logs WHERE id = ? AND session_id = ?`, logID, sessionID) if err != nil { return fmt.Errorf("Log löschen: %w", err) } rows, _ := result.RowsAffected() if rows == 0 { return sql.ErrNoRows } return nil } // GetLastLog gibt die letzten Werte einer Übung für einen Nutzer zurück. func (s *Store) GetLastLog(exerciseID, userID int64) (*model.LastLogResponse, error) { var resp model.LastLogResponse err := s.db.QueryRow(` SELECT sl.weight_kg, sl.reps FROM session_logs sl JOIN sessions s ON s.id = sl.session_id WHERE sl.exercise_id = ? AND s.user_id = ? ORDER BY sl.logged_at DESC LIMIT 1`, exerciseID, userID, ).Scan(&resp.WeightKg, &resp.Reps) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("Letzten Log abfragen: %w", err) } return &resp, nil } // checkSessionOpen prüft ob eine Session offen ist. func (s *Store) checkSessionOpen(sessionID int64) error { var endedAt *string err := s.db.QueryRow(`SELECT ended_at FROM sessions WHERE id = ?`, sessionID).Scan(&endedAt) if err == sql.ErrNoRows { return fmt.Errorf("Session %d existiert nicht", sessionID) } if err != nil { return fmt.Errorf("Session prüfen: %w", err) } if endedAt != nil { return fmt.Errorf("SESSION_CLOSED: Session ist bereits beendet") } return nil } // getLog gibt einen einzelnen Log-Eintrag zurück. func (s *Store) getLog(id int64) (*model.SessionLog, error) { var log model.SessionLog err := s.db.QueryRow(` SELECT id, session_id, exercise_id, exercise_name, set_number, weight_kg, reps, note, logged_at FROM session_logs WHERE id = ?`, id, ).Scan(&log.ID, &log.SessionID, &log.ExerciseID, &log.ExerciseName, &log.SetNumber, &log.WeightKg, &log.Reps, &log.Note, &log.LoggedAt) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("Log abfragen: %w", err) } return &log, nil } // getSessionLogs gibt alle Logs einer Session zurück. func (s *Store) getSessionLogs(sessionID int64) ([]model.SessionLog, error) { rows, err := s.db.Query(` SELECT id, session_id, exercise_id, exercise_name, set_number, weight_kg, reps, note, logged_at FROM session_logs WHERE session_id = ? ORDER BY exercise_id, set_number`, sessionID, ) if err != nil { return nil, fmt.Errorf("Logs abfragen: %w", err) } defer rows.Close() var logs []model.SessionLog for rows.Next() { var log model.SessionLog if err := rows.Scan(&log.ID, &log.SessionID, &log.ExerciseID, &log.ExerciseName, &log.SetNumber, &log.WeightKg, &log.Reps, &log.Note, &log.LoggedAt); err != nil { return nil, fmt.Errorf("Log scannen: %w", err) } logs = append(logs, log) } if logs == nil { logs = []model.SessionLog{} } return logs, rows.Err() }