Initial commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
105
backend/internal/store/exercise_store.go
Executable file
105
backend/internal/store/exercise_store.go
Executable file
@@ -0,0 +1,105 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"krafttrainer/internal/model"
|
||||
)
|
||||
|
||||
// ListExercises gibt alle nicht-gelöschten Übungen zurück, optional gefiltert.
|
||||
func (s *Store) ListExercises(muscleGroup, query string) ([]model.Exercise, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, name, description, muscle_group, weight_step_kg, created_at, updated_at
|
||||
FROM exercises
|
||||
WHERE deleted_at IS NULL
|
||||
AND (muscle_group = ? OR ? = '')
|
||||
AND (name LIKE '%' || ? || '%' OR ? = '')
|
||||
ORDER BY name`,
|
||||
muscleGroup, muscleGroup, query, query,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übungen abfragen: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var exercises []model.Exercise
|
||||
for rows.Next() {
|
||||
var e model.Exercise
|
||||
if err := rows.Scan(&e.ID, &e.Name, &e.Description, &e.MuscleGroup, &e.WeightStepKg, &e.CreatedAt, &e.UpdatedAt); err != nil {
|
||||
return nil, fmt.Errorf("Übung scannen: %w", err)
|
||||
}
|
||||
exercises = append(exercises, e)
|
||||
}
|
||||
if exercises == nil {
|
||||
exercises = []model.Exercise{}
|
||||
}
|
||||
return exercises, rows.Err()
|
||||
}
|
||||
|
||||
// GetExercise gibt eine einzelne Übung zurück.
|
||||
func (s *Store) GetExercise(id int64) (*model.Exercise, error) {
|
||||
var e model.Exercise
|
||||
err := s.db.QueryRow(`
|
||||
SELECT id, name, description, muscle_group, weight_step_kg, created_at, updated_at, deleted_at
|
||||
FROM exercises WHERE id = ?`, id,
|
||||
).Scan(&e.ID, &e.Name, &e.Description, &e.MuscleGroup, &e.WeightStepKg, &e.CreatedAt, &e.UpdatedAt, &e.DeletedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übung abfragen: %w", err)
|
||||
}
|
||||
return &e, nil
|
||||
}
|
||||
|
||||
// CreateExercise legt eine neue Übung an und gibt sie zurück.
|
||||
func (s *Store) CreateExercise(req *model.CreateExerciseRequest) (*model.Exercise, error) {
|
||||
result, err := s.db.Exec(`
|
||||
INSERT INTO exercises (name, description, muscle_group, weight_step_kg)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
req.Name, req.Description, req.MuscleGroup, *req.WeightStepKg,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übung erstellen: %w", err)
|
||||
}
|
||||
|
||||
id, _ := result.LastInsertId()
|
||||
return s.GetExercise(id)
|
||||
}
|
||||
|
||||
// UpdateExercise aktualisiert eine Übung und gibt sie zurück.
|
||||
func (s *Store) UpdateExercise(id int64, req *model.CreateExerciseRequest) (*model.Exercise, error) {
|
||||
result, err := s.db.Exec(`
|
||||
UPDATE exercises
|
||||
SET name = ?, description = ?, muscle_group = ?, weight_step_kg = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND deleted_at IS NULL`,
|
||||
req.Name, req.Description, req.MuscleGroup, *req.WeightStepKg, id,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übung aktualisieren: %w", err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return s.GetExercise(id)
|
||||
}
|
||||
|
||||
// SoftDeleteExercise markiert eine Übung als gelöscht.
|
||||
func (s *Store) SoftDeleteExercise(id int64) error {
|
||||
result, err := s.db.Exec(`
|
||||
UPDATE exercises SET deleted_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND deleted_at IS NULL`, id,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Übung löschen: %w", err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
269
backend/internal/store/session_store.go
Executable file
269
backend/internal/store/session_store.go
Executable file
@@ -0,0 +1,269 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"krafttrainer/internal/model"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CreateSession startet eine neue Trainingseinheit.
|
||||
func (s *Store) CreateSession(setID int64) (*model.Session, error) {
|
||||
// Set prüfen
|
||||
var setName string
|
||||
err := s.db.QueryRow(`SELECT name FROM training_sets WHERE id = ? AND deleted_at IS NULL`, setID).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) VALUES (?)`, setID)
|
||||
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.
|
||||
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.
|
||||
func (s *Store) EndSession(id int64, note string) (*model.Session, error) {
|
||||
result, err := s.db.Exec(`
|
||||
UPDATE sessions SET ended_at = CURRENT_TIMESTAMP, note = ?
|
||||
WHERE id = ? AND ended_at IS NULL`, note, id,
|
||||
)
|
||||
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 zurück (neueste zuerst).
|
||||
func (s *Store) ListSessions(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
|
||||
ORDER BY s.started_at DESC
|
||||
LIMIT ? OFFSET ?`, 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) {
|
||||
// Session offen?
|
||||
if err := s.checkSessionOpen(sessionID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Übungsname denormalisiert speichern
|
||||
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
|
||||
}
|
||||
|
||||
// Log gehört zur Session?
|
||||
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
|
||||
}
|
||||
|
||||
// Partielle Updates
|
||||
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 zurück.
|
||||
func (s *Store) GetLastLog(exerciseID int64) (*model.LastLogResponse, error) {
|
||||
var resp model.LastLogResponse
|
||||
err := s.db.QueryRow(`
|
||||
SELECT weight_kg, reps FROM session_logs
|
||||
WHERE exercise_id = ?
|
||||
ORDER BY logged_at DESC LIMIT 1`, exerciseID,
|
||||
).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()
|
||||
}
|
||||
200
backend/internal/store/set_store.go
Executable file
200
backend/internal/store/set_store.go
Executable file
@@ -0,0 +1,200 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"krafttrainer/internal/model"
|
||||
)
|
||||
|
||||
// ListSets gibt alle nicht-gelöschten Sets mit ihren Übungen zurück.
|
||||
func (s *Store) ListSets() ([]model.TrainingSet, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, name, created_at FROM training_sets
|
||||
WHERE deleted_at IS NULL ORDER BY name`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Sets abfragen: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var sets []model.TrainingSet
|
||||
for rows.Next() {
|
||||
var ts model.TrainingSet
|
||||
if err := rows.Scan(&ts.ID, &ts.Name, &ts.CreatedAt); err != nil {
|
||||
return nil, fmt.Errorf("Set scannen: %w", err)
|
||||
}
|
||||
sets = append(sets, ts)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sets == nil {
|
||||
sets = []model.TrainingSet{}
|
||||
}
|
||||
|
||||
for i := range sets {
|
||||
exercises, err := s.getSetExercises(sets[i].ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sets[i].Exercises = exercises
|
||||
}
|
||||
return sets, nil
|
||||
}
|
||||
|
||||
// GetSet gibt ein einzelnes Set mit Übungen zurück.
|
||||
func (s *Store) GetSet(id int64) (*model.TrainingSet, error) {
|
||||
var ts model.TrainingSet
|
||||
err := s.db.QueryRow(`
|
||||
SELECT id, name, created_at, deleted_at FROM training_sets WHERE id = ?`, id,
|
||||
).Scan(&ts.ID, &ts.Name, &ts.CreatedAt, &ts.DeletedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Set abfragen: %w", err)
|
||||
}
|
||||
|
||||
exercises, err := s.getSetExercises(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ts.Exercises = exercises
|
||||
return &ts, nil
|
||||
}
|
||||
|
||||
// CreateSet legt ein neues Set an (in einer Transaktion).
|
||||
func (s *Store) CreateSet(req *model.CreateSetRequest) (*model.TrainingSet, error) {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Transaktion starten: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Prüfen ob alle Übungen existieren
|
||||
for _, eid := range req.ExerciseIDs {
|
||||
var exists bool
|
||||
err := tx.QueryRow(`SELECT EXISTS(SELECT 1 FROM exercises WHERE id = ? AND deleted_at IS NULL)`, eid).Scan(&exists)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übung prüfen: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("Übung %d existiert nicht", eid)
|
||||
}
|
||||
}
|
||||
|
||||
result, err := tx.Exec(`INSERT INTO training_sets (name) VALUES (?)`, req.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Set erstellen: %w", err)
|
||||
}
|
||||
|
||||
id, _ := result.LastInsertId()
|
||||
|
||||
for pos, eid := range req.ExerciseIDs {
|
||||
_, err := tx.Exec(`INSERT INTO set_exercises (set_id, exercise_id, position) VALUES (?, ?, ?)`, id, eid, pos)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Set-Übung zuordnen: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, fmt.Errorf("Transaktion committen: %w", err)
|
||||
}
|
||||
return s.GetSet(id)
|
||||
}
|
||||
|
||||
// UpdateSet aktualisiert ein Set (Name + Übungszuordnungen).
|
||||
func (s *Store) UpdateSet(id int64, req *model.UpdateSetRequest) (*model.TrainingSet, error) {
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Transaktion starten: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Prüfen ob Set existiert
|
||||
var exists bool
|
||||
err = tx.QueryRow(`SELECT EXISTS(SELECT 1 FROM training_sets WHERE id = ? AND deleted_at IS NULL)`, id).Scan(&exists)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Set prüfen: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Prüfen ob alle Übungen existieren
|
||||
for _, eid := range req.ExerciseIDs {
|
||||
var eExists bool
|
||||
err := tx.QueryRow(`SELECT EXISTS(SELECT 1 FROM exercises WHERE id = ? AND deleted_at IS NULL)`, eid).Scan(&eExists)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übung prüfen: %w", err)
|
||||
}
|
||||
if !eExists {
|
||||
return nil, fmt.Errorf("Übung %d existiert nicht", eid)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`UPDATE training_sets SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, req.Name, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Set aktualisieren: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`DELETE FROM set_exercises WHERE set_id = ?`, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Set-Übungen löschen: %w", err)
|
||||
}
|
||||
|
||||
for pos, eid := range req.ExerciseIDs {
|
||||
_, err := tx.Exec(`INSERT INTO set_exercises (set_id, exercise_id, position) VALUES (?, ?, ?)`, id, eid, pos)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Set-Übung zuordnen: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, fmt.Errorf("Transaktion committen: %w", err)
|
||||
}
|
||||
return s.GetSet(id)
|
||||
}
|
||||
|
||||
// SoftDeleteSet markiert ein Set als gelöscht.
|
||||
func (s *Store) SoftDeleteSet(id int64) error {
|
||||
result, err := s.db.Exec(`
|
||||
UPDATE training_sets SET deleted_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND deleted_at IS NULL`, id,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Set löschen: %w", err)
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSetExercises lädt die Übungen eines Sets sortiert nach Position.
|
||||
func (s *Store) getSetExercises(setID int64) ([]model.Exercise, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT e.id, e.name, e.description, e.muscle_group, e.weight_step_kg, e.created_at, e.updated_at
|
||||
FROM exercises e
|
||||
JOIN set_exercises se ON se.exercise_id = e.id
|
||||
WHERE se.set_id = ?
|
||||
ORDER BY se.position`, setID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Set-Übungen abfragen: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var exercises []model.Exercise
|
||||
for rows.Next() {
|
||||
var e model.Exercise
|
||||
if err := rows.Scan(&e.ID, &e.Name, &e.Description, &e.MuscleGroup, &e.WeightStepKg, &e.CreatedAt, &e.UpdatedAt); err != nil {
|
||||
return nil, fmt.Errorf("Übung scannen: %w", err)
|
||||
}
|
||||
exercises = append(exercises, e)
|
||||
}
|
||||
if exercises == nil {
|
||||
exercises = []model.Exercise{}
|
||||
}
|
||||
return exercises, rows.Err()
|
||||
}
|
||||
85
backend/internal/store/stats_store.go
Executable file
85
backend/internal/store/stats_store.go
Executable file
@@ -0,0 +1,85 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"krafttrainer/internal/model"
|
||||
)
|
||||
|
||||
// StatsOverview enthält die Gesamtübersicht.
|
||||
type StatsOverview struct {
|
||||
TotalSessions int `json:"total_sessions"`
|
||||
TotalVolumeKg float64 `json:"total_volume_kg"`
|
||||
SessionsThisWeek int `json:"sessions_this_week"`
|
||||
Exercises []model.ExerciseStats `json:"exercises"`
|
||||
}
|
||||
|
||||
// GetStatsOverview gibt die Gesamtstatistik zurück.
|
||||
func (s *Store) GetStatsOverview() (*StatsOverview, error) {
|
||||
var overview StatsOverview
|
||||
|
||||
err := s.db.QueryRow(`
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM sessions WHERE ended_at IS NOT NULL),
|
||||
(SELECT COALESCE(SUM(weight_kg * reps), 0) FROM session_logs),
|
||||
(SELECT COUNT(*) FROM sessions WHERE ended_at IS NOT NULL AND started_at >= date('now', '-7 days'))
|
||||
`).Scan(&overview.TotalSessions, &overview.TotalVolumeKg, &overview.SessionsThisWeek)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übersicht abfragen: %w", err)
|
||||
}
|
||||
|
||||
rows, err := s.db.Query(`
|
||||
SELECT
|
||||
sl.exercise_id,
|
||||
sl.exercise_name,
|
||||
MAX(sl.weight_kg) as max_weight_kg,
|
||||
SUM(sl.weight_kg * sl.reps) as total_volume_kg,
|
||||
COUNT(*) as total_sets,
|
||||
MAX(sl.logged_at) as last_trained
|
||||
FROM session_logs sl
|
||||
GROUP BY sl.exercise_id
|
||||
ORDER BY last_trained DESC`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übungs-Stats abfragen: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var es model.ExerciseStats
|
||||
if err := rows.Scan(&es.ExerciseID, &es.ExerciseName, &es.MaxWeightKg, &es.TotalVolumeKg, &es.TotalSets, &es.LastTrained); err != nil {
|
||||
return nil, fmt.Errorf("Übungs-Stats scannen: %w", err)
|
||||
}
|
||||
overview.Exercises = append(overview.Exercises, es)
|
||||
}
|
||||
if overview.Exercises == nil {
|
||||
overview.Exercises = []model.ExerciseStats{}
|
||||
}
|
||||
return &overview, rows.Err()
|
||||
}
|
||||
|
||||
// GetExerciseHistory gibt die letzten N Logs einer Übung zurück.
|
||||
func (s *Store) GetExerciseHistory(exerciseID int64, limit int) ([]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 exercise_id = ?
|
||||
ORDER BY logged_at DESC
|
||||
LIMIT ?`, exerciseID, limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übungshistorie 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()
|
||||
}
|
||||
37
backend/internal/store/store.go
Executable file
37
backend/internal/store/store.go
Executable file
@@ -0,0 +1,37 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// Store kapselt den Datenbankzugriff.
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// New erstellt einen neuen Store und konfiguriert SQLite.
|
||||
func New(dbPath string) (*Store, error) {
|
||||
db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_foreign_keys=ON")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Datenbank öffnen: %w", err)
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("Datenbank-Verbindung prüfen: %w", err)
|
||||
}
|
||||
|
||||
return &Store{db: db}, nil
|
||||
}
|
||||
|
||||
// DB gibt die zugrundeliegende Datenbankverbindung zurück (für Migrations).
|
||||
func (s *Store) DB() *sql.DB {
|
||||
return s.db
|
||||
}
|
||||
|
||||
// Close schliesst die Datenbankverbindung.
|
||||
func (s *Store) Close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user