Add exercise numbers, image uploads, version display, session resume, and training sparklines
- Exercise number (UF#): optional field on exercises, displayed in cards, training, and sets - Import training plan numbers via migration 005 (UPDATE by name) - Exercise images: JPG upload with multi-image support per exercise (migration 006) - Version endpoint (GET /api/v1/version) with ldflags injection in Makefile and Dockerfile - Version displayed on settings page - Session resume: GET /api/v1/sessions/active endpoint, auto-resume on training page load - Block new session while one is active (409 Conflict) - e1RM sparkline chart per exercise during training (Epley formula) - Fix CORS: add X-User-ID to allowed headers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
74
backend/internal/store/image_store.go
Normal file
74
backend/internal/store/image_store.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"krafttrainer/internal/model"
|
||||
)
|
||||
|
||||
// ListExerciseImages gibt alle Bilder einer Übung zurück.
|
||||
func (s *Store) ListExerciseImages(exerciseID int64) ([]model.ExerciseImage, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, exercise_id, filename, sort_order, created_at
|
||||
FROM exercise_images
|
||||
WHERE exercise_id = ?
|
||||
ORDER BY sort_order, id`, exerciseID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Bilder abfragen: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var images []model.ExerciseImage
|
||||
for rows.Next() {
|
||||
var img model.ExerciseImage
|
||||
if err := rows.Scan(&img.ID, &img.ExerciseID, &img.Filename, &img.SortOrder, &img.CreatedAt); err != nil {
|
||||
return nil, fmt.Errorf("Bild scannen: %w", err)
|
||||
}
|
||||
images = append(images, img)
|
||||
}
|
||||
if images == nil {
|
||||
images = []model.ExerciseImage{}
|
||||
}
|
||||
return images, rows.Err()
|
||||
}
|
||||
|
||||
// CreateExerciseImage speichert einen Bild-Eintrag.
|
||||
func (s *Store) CreateExerciseImage(exerciseID int64, filename string) (*model.ExerciseImage, error) {
|
||||
// sort_order = bisherige Anzahl
|
||||
var count int
|
||||
s.db.QueryRow(`SELECT COUNT(*) FROM exercise_images WHERE exercise_id = ?`, exerciseID).Scan(&count)
|
||||
|
||||
result, err := s.db.Exec(`
|
||||
INSERT INTO exercise_images (exercise_id, filename, sort_order)
|
||||
VALUES (?, ?, ?)`, exerciseID, filename, count,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Bild speichern: %w", err)
|
||||
}
|
||||
|
||||
id, _ := result.LastInsertId()
|
||||
var img model.ExerciseImage
|
||||
err = s.db.QueryRow(`
|
||||
SELECT id, exercise_id, filename, sort_order, created_at
|
||||
FROM exercise_images WHERE id = ?`, id,
|
||||
).Scan(&img.ID, &img.ExerciseID, &img.Filename, &img.SortOrder, &img.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Bild abfragen: %w", err)
|
||||
}
|
||||
return &img, nil
|
||||
}
|
||||
|
||||
// DeleteExerciseImage löscht ein Bild und gibt den Dateinamen zurück.
|
||||
func (s *Store) DeleteExerciseImage(imageID int64) (string, error) {
|
||||
var filename string
|
||||
err := s.db.QueryRow(`SELECT filename FROM exercise_images WHERE id = ?`, imageID).Scan(&filename)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Bild nicht gefunden: %w", err)
|
||||
}
|
||||
|
||||
_, err = s.db.Exec(`DELETE FROM exercise_images WHERE id = ?`, imageID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Bild löschen: %w", err)
|
||||
}
|
||||
return filename, nil
|
||||
}
|
||||
Reference in New Issue
Block a user