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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user