Add multi-user support with export feature
- New users table (migration 004) with user_id on exercises, training_sets, sessions
- User CRUD endpoints (GET/POST /api/v1/users, DELETE /api/v1/users/{id})
- All existing endpoints scoped to X-User-ID header
- CSV export endpoint (GET /api/v1/export) for completed sessions
- UserGate in PageShell: blocks app until a user is selected
- Settings page for managing users (create, switch, delete)
- BottomNav/Sidebar updated with settings navigation
- Fix: nil pointer panic in handleDeleteUser on success path
- Fix: export download now uses fetch with X-User-ID header instead of window.location.href
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,16 +6,17 @@ import (
|
||||
"krafttrainer/internal/model"
|
||||
)
|
||||
|
||||
// ListExercises gibt alle nicht-gelöschten Übungen zurück, optional gefiltert.
|
||||
func (s *Store) ListExercises(muscleGroup, query string) ([]model.Exercise, error) {
|
||||
// ListExercises gibt alle nicht-gelöschten Übungen eines Nutzers zurück, optional gefiltert.
|
||||
func (s *Store) ListExercises(userID int64, 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 user_id = ?
|
||||
AND (muscle_group = ? OR ? = '')
|
||||
AND (name LIKE '%' || ? || '%' OR ? = '')
|
||||
ORDER BY name`,
|
||||
muscleGroup, muscleGroup, query, query,
|
||||
userID, muscleGroup, muscleGroup, query, query,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übungen abfragen: %w", err)
|
||||
@@ -36,7 +37,7 @@ func (s *Store) ListExercises(muscleGroup, query string) ([]model.Exercise, erro
|
||||
return exercises, rows.Err()
|
||||
}
|
||||
|
||||
// GetExercise gibt eine einzelne Übung zurück.
|
||||
// GetExercise gibt eine einzelne Übung zurück (intern, ohne User-Scope).
|
||||
func (s *Store) GetExercise(id int64) (*model.Exercise, error) {
|
||||
var e model.Exercise
|
||||
err := s.db.QueryRow(`
|
||||
@@ -53,11 +54,11 @@ func (s *Store) GetExercise(id int64) (*model.Exercise, error) {
|
||||
}
|
||||
|
||||
// CreateExercise legt eine neue Übung an und gibt sie zurück.
|
||||
func (s *Store) CreateExercise(req *model.CreateExerciseRequest) (*model.Exercise, error) {
|
||||
func (s *Store) CreateExercise(userID int64, 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,
|
||||
INSERT INTO exercises (name, description, muscle_group, weight_step_kg, user_id)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
req.Name, req.Description, req.MuscleGroup, *req.WeightStepKg, userID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übung erstellen: %w", err)
|
||||
@@ -67,14 +68,14 @@ func (s *Store) CreateExercise(req *model.CreateExerciseRequest) (*model.Exercis
|
||||
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) {
|
||||
// UpdateExercise aktualisiert eine Übung eines Nutzers und gibt sie zurück.
|
||||
func (s *Store) UpdateExercise(id, userID 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,
|
||||
WHERE id = ? AND user_id = ? AND deleted_at IS NULL`,
|
||||
req.Name, req.Description, req.MuscleGroup, *req.WeightStepKg, id, userID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übung aktualisieren: %w", err)
|
||||
@@ -87,11 +88,11 @@ func (s *Store) UpdateExercise(id int64, req *model.CreateExerciseRequest) (*mod
|
||||
return s.GetExercise(id)
|
||||
}
|
||||
|
||||
// SoftDeleteExercise markiert eine Übung als gelöscht.
|
||||
func (s *Store) SoftDeleteExercise(id int64) error {
|
||||
// SoftDeleteExercise markiert eine Übung eines Nutzers als gelöscht.
|
||||
func (s *Store) SoftDeleteExercise(id, userID 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,
|
||||
WHERE id = ? AND user_id = ? AND deleted_at IS NULL`, id, userID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Übung löschen: %w", err)
|
||||
|
||||
40
backend/internal/store/export_store.go
Normal file
40
backend/internal/store/export_store.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExportRow repräsentiert eine Zeile im Trainingsexport.
|
||||
type ExportRow struct {
|
||||
SessionStarted time.Time
|
||||
ExerciseName string
|
||||
SetNumber int
|
||||
WeightKg float64
|
||||
Reps int
|
||||
Note string
|
||||
}
|
||||
|
||||
// ExportLogs gibt alle Logs abgeschlossener Sessions eines Nutzers zurück (für AI-Export).
|
||||
func (s *Store) ExportLogs(userID int64) ([]ExportRow, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT s.started_at, sl.exercise_name, sl.set_number, sl.weight_kg, sl.reps, sl.note
|
||||
FROM session_logs sl
|
||||
JOIN sessions s ON s.id = sl.session_id
|
||||
WHERE s.ended_at IS NOT NULL AND s.user_id = ?
|
||||
ORDER BY s.started_at, sl.exercise_name, sl.set_number`, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Export abfragen: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []ExportRow
|
||||
for rows.Next() {
|
||||
var row ExportRow
|
||||
if err := rows.Scan(&row.SessionStarted, &row.ExerciseName, &row.SetNumber, &row.WeightKg, &row.Reps, &row.Note); err != nil {
|
||||
return nil, fmt.Errorf("Export scannen: %w", err)
|
||||
}
|
||||
result = append(result, row)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
@@ -7,11 +7,10 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CreateSession startet eine neue Trainingseinheit.
|
||||
func (s *Store) CreateSession(setID int64) (*model.Session, error) {
|
||||
// Set prüfen
|
||||
// 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 deleted_at IS NULL`, setID).Scan(&setName)
|
||||
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)
|
||||
}
|
||||
@@ -19,7 +18,7 @@ func (s *Store) CreateSession(setID int64) (*model.Session, error) {
|
||||
return nil, fmt.Errorf("Set prüfen: %w", err)
|
||||
}
|
||||
|
||||
result, err := s.db.Exec(`INSERT INTO sessions (set_id) VALUES (?)`, setID)
|
||||
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)
|
||||
}
|
||||
@@ -28,7 +27,7 @@ func (s *Store) CreateSession(setID int64) (*model.Session, error) {
|
||||
return s.GetSession(id)
|
||||
}
|
||||
|
||||
// GetSession gibt eine Session mit allen Logs zurück.
|
||||
// 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(`
|
||||
@@ -52,11 +51,11 @@ func (s *Store) GetSession(id int64) (*model.Session, error) {
|
||||
return &sess, nil
|
||||
}
|
||||
|
||||
// EndSession beendet eine Session.
|
||||
func (s *Store) EndSession(id int64, note string) (*model.Session, error) {
|
||||
// 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 ended_at IS NULL`, note, id,
|
||||
WHERE id = ? AND user_id = ? AND ended_at IS NULL`, note, id, userID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Session beenden: %w", err)
|
||||
@@ -68,14 +67,15 @@ func (s *Store) EndSession(id int64, note string) (*model.Session, error) {
|
||||
return s.GetSession(id)
|
||||
}
|
||||
|
||||
// ListSessions gibt paginierte Sessions zurück (neueste zuerst).
|
||||
func (s *Store) ListSessions(limit, offset int) ([]model.Session, error) {
|
||||
// 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 ?`, limit, offset,
|
||||
LIMIT ? OFFSET ?`, userID, limit, offset,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Sessions abfragen: %w", err)
|
||||
@@ -98,12 +98,10 @@ func (s *Store) ListSessions(limit, offset int) ([]model.Session, error) {
|
||||
|
||||
// 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 {
|
||||
@@ -135,7 +133,6 @@ func (s *Store) UpdateLog(sessionID, logID int64, req *model.UpdateLogRequest) (
|
||||
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 {
|
||||
@@ -145,7 +142,6 @@ func (s *Store) UpdateLog(sessionID, logID int64, req *model.UpdateLogRequest) (
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Partielle Updates
|
||||
updates := []string{}
|
||||
args := []any{}
|
||||
if req.WeightKg != nil {
|
||||
@@ -192,13 +188,15 @@ func (s *Store) DeleteLog(sessionID, logID int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLastLog gibt die letzten Werte einer Übung zurück.
|
||||
func (s *Store) GetLastLog(exerciseID int64) (*model.LastLogResponse, error) {
|
||||
// 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 weight_kg, reps FROM session_logs
|
||||
WHERE exercise_id = ?
|
||||
ORDER BY logged_at DESC LIMIT 1`, exerciseID,
|
||||
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
|
||||
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"krafttrainer/internal/model"
|
||||
)
|
||||
|
||||
// ListSets gibt alle nicht-gelöschten Sets mit ihren Übungen zurück.
|
||||
func (s *Store) ListSets() ([]model.TrainingSet, error) {
|
||||
// ListSets gibt alle nicht-gelöschten Sets eines Nutzers mit ihren Übungen zurück.
|
||||
func (s *Store) ListSets(userID int64) ([]model.TrainingSet, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, name, created_at FROM training_sets
|
||||
WHERE deleted_at IS NULL ORDER BY name`)
|
||||
WHERE deleted_at IS NULL AND user_id = ? ORDER BY name`, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Sets abfragen: %w", err)
|
||||
}
|
||||
@@ -41,7 +41,7 @@ func (s *Store) ListSets() ([]model.TrainingSet, error) {
|
||||
return sets, nil
|
||||
}
|
||||
|
||||
// GetSet gibt ein einzelnes Set mit Übungen zurück.
|
||||
// GetSet gibt ein einzelnes Set mit Übungen zurück (intern, ohne User-Scope).
|
||||
func (s *Store) GetSet(id int64) (*model.TrainingSet, error) {
|
||||
var ts model.TrainingSet
|
||||
err := s.db.QueryRow(`
|
||||
@@ -63,17 +63,16 @@ func (s *Store) GetSet(id int64) (*model.TrainingSet, error) {
|
||||
}
|
||||
|
||||
// CreateSet legt ein neues Set an (in einer Transaktion).
|
||||
func (s *Store) CreateSet(req *model.CreateSetRequest) (*model.TrainingSet, error) {
|
||||
func (s *Store) CreateSet(userID int64, 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)
|
||||
err := tx.QueryRow(`SELECT EXISTS(SELECT 1 FROM exercises WHERE id = ? AND user_id = ? AND deleted_at IS NULL)`, eid, userID).Scan(&exists)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übung prüfen: %w", err)
|
||||
}
|
||||
@@ -82,7 +81,7 @@ func (s *Store) CreateSet(req *model.CreateSetRequest) (*model.TrainingSet, erro
|
||||
}
|
||||
}
|
||||
|
||||
result, err := tx.Exec(`INSERT INTO training_sets (name) VALUES (?)`, req.Name)
|
||||
result, err := tx.Exec(`INSERT INTO training_sets (name, user_id) VALUES (?, ?)`, req.Name, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Set erstellen: %w", err)
|
||||
}
|
||||
@@ -102,17 +101,16 @@ func (s *Store) CreateSet(req *model.CreateSetRequest) (*model.TrainingSet, erro
|
||||
return s.GetSet(id)
|
||||
}
|
||||
|
||||
// UpdateSet aktualisiert ein Set (Name + Übungszuordnungen).
|
||||
func (s *Store) UpdateSet(id int64, req *model.UpdateSetRequest) (*model.TrainingSet, error) {
|
||||
// UpdateSet aktualisiert ein Set eines Nutzers (Name + Übungszuordnungen).
|
||||
func (s *Store) UpdateSet(id, userID 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)
|
||||
err = tx.QueryRow(`SELECT EXISTS(SELECT 1 FROM training_sets WHERE id = ? AND user_id = ? AND deleted_at IS NULL)`, id, userID).Scan(&exists)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Set prüfen: %w", err)
|
||||
}
|
||||
@@ -120,10 +118,9 @@ func (s *Store) UpdateSet(id int64, req *model.UpdateSetRequest) (*model.Trainin
|
||||
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)
|
||||
err := tx.QueryRow(`SELECT EXISTS(SELECT 1 FROM exercises WHERE id = ? AND user_id = ? AND deleted_at IS NULL)`, eid, userID).Scan(&eExists)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übung prüfen: %w", err)
|
||||
}
|
||||
@@ -155,11 +152,11 @@ func (s *Store) UpdateSet(id int64, req *model.UpdateSetRequest) (*model.Trainin
|
||||
return s.GetSet(id)
|
||||
}
|
||||
|
||||
// SoftDeleteSet markiert ein Set als gelöscht.
|
||||
func (s *Store) SoftDeleteSet(id int64) error {
|
||||
// SoftDeleteSet markiert ein Set eines Nutzers als gelöscht.
|
||||
func (s *Store) SoftDeleteSet(id, userID 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,
|
||||
WHERE id = ? AND user_id = ? AND deleted_at IS NULL`, id, userID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Set löschen: %w", err)
|
||||
|
||||
@@ -13,16 +13,16 @@ type StatsOverview struct {
|
||||
Exercises []model.ExerciseStats `json:"exercises"`
|
||||
}
|
||||
|
||||
// GetStatsOverview gibt die Gesamtstatistik zurück.
|
||||
func (s *Store) GetStatsOverview() (*StatsOverview, error) {
|
||||
// GetStatsOverview gibt die Gesamtstatistik eines Nutzers zurück.
|
||||
func (s *Store) GetStatsOverview(userID int64) (*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)
|
||||
(SELECT COUNT(*) FROM sessions WHERE user_id = ? AND ended_at IS NOT NULL),
|
||||
(SELECT COALESCE(SUM(sl.weight_kg * sl.reps), 0) FROM session_logs sl JOIN sessions s ON s.id = sl.session_id WHERE s.user_id = ?),
|
||||
(SELECT COUNT(*) FROM sessions WHERE user_id = ? AND ended_at IS NOT NULL AND started_at >= date('now', '-7 days'))
|
||||
`, userID, userID, userID).Scan(&overview.TotalSessions, &overview.TotalVolumeKg, &overview.SessionsThisWeek)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übersicht abfragen: %w", err)
|
||||
}
|
||||
@@ -36,8 +36,10 @@ func (s *Store) GetStatsOverview() (*StatsOverview, error) {
|
||||
COUNT(*) as total_sets,
|
||||
MAX(sl.logged_at) as last_trained
|
||||
FROM session_logs sl
|
||||
JOIN sessions s ON s.id = sl.session_id
|
||||
WHERE s.user_id = ?
|
||||
GROUP BY sl.exercise_id
|
||||
ORDER BY last_trained DESC`)
|
||||
ORDER BY last_trained DESC`, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übungs-Stats abfragen: %w", err)
|
||||
}
|
||||
@@ -56,14 +58,15 @@ func (s *Store) GetStatsOverview() (*StatsOverview, error) {
|
||||
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) {
|
||||
// GetExerciseHistory gibt die letzten N Logs einer Übung für einen Nutzer zurück.
|
||||
func (s *Store) GetExerciseHistory(exerciseID, userID 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,
|
||||
SELECT sl.id, sl.session_id, sl.exercise_id, sl.exercise_name, sl.set_number, sl.weight_kg, sl.reps, sl.note, sl.logged_at
|
||||
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 ?`, exerciseID, userID, limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Übungshistorie abfragen: %w", err)
|
||||
|
||||
73
backend/internal/store/user_store.go
Normal file
73
backend/internal/store/user_store.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"krafttrainer/internal/model"
|
||||
)
|
||||
|
||||
// ListUsers gibt alle Nutzer zurück.
|
||||
func (s *Store) ListUsers() ([]model.User, error) {
|
||||
rows, err := s.db.Query(`SELECT id, name, created_at FROM users ORDER BY created_at`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Nutzer abfragen: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var users []model.User
|
||||
for rows.Next() {
|
||||
var u model.User
|
||||
if err := rows.Scan(&u.ID, &u.Name, &u.CreatedAt); err != nil {
|
||||
return nil, fmt.Errorf("Nutzer scannen: %w", err)
|
||||
}
|
||||
users = append(users, u)
|
||||
}
|
||||
if users == nil {
|
||||
users = []model.User{}
|
||||
}
|
||||
return users, rows.Err()
|
||||
}
|
||||
|
||||
// CreateUser legt einen neuen Nutzer an.
|
||||
func (s *Store) CreateUser(name string) (*model.User, error) {
|
||||
result, err := s.db.Exec(`INSERT INTO users (name) VALUES (?)`, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Nutzer erstellen: %w", err)
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
return s.getUser(id)
|
||||
}
|
||||
|
||||
// DeleteUser löscht einen Nutzer, sofern noch mindestens ein weiterer existiert.
|
||||
func (s *Store) DeleteUser(id int64) error {
|
||||
var count int
|
||||
if err := s.db.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&count); err != nil {
|
||||
return fmt.Errorf("Nutzeranzahl prüfen: %w", err)
|
||||
}
|
||||
if count <= 1 {
|
||||
return fmt.Errorf("LAST_USER: letzter Nutzer kann nicht gelöscht werden")
|
||||
}
|
||||
|
||||
result, err := s.db.Exec(`DELETE FROM users WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Nutzer löschen: %w", err)
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) getUser(id int64) (*model.User, error) {
|
||||
var u model.User
|
||||
err := s.db.QueryRow(`SELECT id, name, created_at FROM users WHERE id = ?`, id).
|
||||
Scan(&u.ID, &u.Name, &u.CreatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Nutzer abfragen: %w", err)
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
Reference in New Issue
Block a user