From 77751706344d974ce7fcabe68e0925f7d41a3e2a Mon Sep 17 00:00:00 2001
From: Frank
Date: Wed, 14 Jan 2026 18:52:29 +0100
Subject: [PATCH] moved some basic value lists to database
---
backend/character/handlers.go | 125 +++++++++++++++++++
backend/character/handlers_test.go | 140 ++++++++++++++++++++++
backend/character/routes.go | 1 +
backend/cmd/main.go | 10 ++
backend/gsmaster/misclookup.go | 77 ++++++++++++
backend/gsmaster/misclookup_test.go | 123 +++++++++++++++++++
backend/models/database.go | 1 +
backend/models/model_gsmaster.go | 14 +++
frontend/src/components/DatasheetView.vue | 67 +++++++++--
9 files changed, 550 insertions(+), 8 deletions(-)
create mode 100644 backend/gsmaster/misclookup.go
create mode 100644 backend/gsmaster/misclookup_test.go
diff --git a/backend/character/handlers.go b/backend/character/handlers.go
index 3252bb5..d40c2e6 100644
--- a/backend/character/handlers.go
+++ b/backend/character/handlers.go
@@ -3307,3 +3307,128 @@ func getStandBonusPoints(stand string) map[string]int {
return make(map[string]int)
}
}
+
+// GetDatasheetOptions returns all available options for datasheet select boxes
+func GetDatasheetOptions(c *gin.Context) {
+ logger.Debug("GetDatasheetOptions aufgerufen")
+
+ characterID := c.Param("id")
+
+ // Load character to get their weapon skills
+ var character models.Char
+ err := character.FirstID(characterID)
+ if err != nil {
+ logger.Error("GetDatasheetOptions: Charakter nicht gefunden - ID: %s, Error: %s", characterID, err.Error())
+ respondWithError(c, http.StatusNotFound, "Character not found")
+ return
+ }
+
+ // Get all available weapons from database
+ var allWeapons []models.Weapon
+ if err := database.DB.Find(&allWeapons).Error; err != nil {
+ logger.Error("GetDatasheetOptions: Fehler beim Laden der Waffen: %s", err.Error())
+ respondWithError(c, http.StatusInternalServerError, "Failed to load weapons")
+ return
+ }
+
+ // Filter weapons based on character's weapon skills
+ characterWeaponSkills := make(map[string]bool)
+ for _, skill := range character.Waffenfertigkeiten {
+ characterWeaponSkills[skill.Name] = true
+ }
+
+ availableWeapons := []string{}
+ for _, weapon := range allWeapons {
+ if characterWeaponSkills[weapon.SkillRequired] {
+ availableWeapons = append(availableWeapons, weapon.Name)
+ }
+ }
+
+ // Load misc lookup data from database
+ genders, err := gsmaster.GetMiscLookupByKey("gender")
+ if err != nil {
+ logger.Error("GetDatasheetOptions: Fehler beim Laden der Geschlechter: %s", err.Error())
+ respondWithError(c, http.StatusInternalServerError, "Failed to load genders")
+ return
+ }
+
+ races, err := gsmaster.GetMiscLookupByKey("races")
+ if err != nil {
+ logger.Error("GetDatasheetOptions: Fehler beim Laden der Rassen: %s", err.Error())
+ respondWithError(c, http.StatusInternalServerError, "Failed to load races")
+ return
+ }
+
+ origins, err := gsmaster.GetMiscLookupByKey("origins")
+ if err != nil {
+ logger.Error("GetDatasheetOptions: Fehler beim Laden der Herkünfte: %s", err.Error())
+ respondWithError(c, http.StatusInternalServerError, "Failed to load origins")
+ return
+ }
+
+ socialClasses, err := gsmaster.GetMiscLookupByKey("social_classes")
+ if err != nil {
+ logger.Error("GetDatasheetOptions: Fehler beim Laden der Stände: %s", err.Error())
+ respondWithError(c, http.StatusInternalServerError, "Failed to load social classes")
+ return
+ }
+
+ faiths, err := gsmaster.GetMiscLookupByKey("faiths")
+ if err != nil {
+ logger.Error("GetDatasheetOptions: Fehler beim Laden der Glaubensrichtungen: %s", err.Error())
+ respondWithError(c, http.StatusInternalServerError, "Failed to load faiths")
+ return
+ }
+
+ handedness, err := gsmaster.GetMiscLookupByKey("handedness")
+ if err != nil {
+ logger.Error("GetDatasheetOptions: Fehler beim Laden der Händigkeiten: %s", err.Error())
+ respondWithError(c, http.StatusInternalServerError, "Failed to load handedness")
+ return
+ }
+
+ // Convert to string arrays
+ genderValues := make([]string, len(genders))
+ for i, g := range genders {
+ genderValues[i] = g.Value
+ }
+
+ raceValues := make([]string, len(races))
+ for i, r := range races {
+ raceValues[i] = r.Value
+ }
+
+ originValues := make([]string, len(origins))
+ for i, o := range origins {
+ originValues[i] = o.Value
+ }
+
+ socialClassValues := make([]string, len(socialClasses))
+ for i, sc := range socialClasses {
+ socialClassValues[i] = sc.Value
+ }
+
+ faithValues := make([]string, len(faiths))
+ for i, f := range faiths {
+ faithValues[i] = f.Value
+ }
+
+ handednessValues := make([]string, len(handedness))
+ for i, h := range handedness {
+ handednessValues[i] = h.Value
+ }
+
+ // Return all options
+ options := gin.H{
+ "gender": genderValues,
+ "races": raceValues,
+ "origins": originValues,
+ "social_classes": socialClassValues,
+ "faiths": faithValues,
+ "handedness": handednessValues,
+ "specializations": availableWeapons,
+ }
+
+ logger.Debug("GetDatasheetOptions: Erfolgreich geladen - %d verfügbare Waffen", len(availableWeapons))
+ c.JSON(http.StatusOK, options)
+}
diff --git a/backend/character/handlers_test.go b/backend/character/handlers_test.go
index 25be692..b4ce189 100644
--- a/backend/character/handlers_test.go
+++ b/backend/character/handlers_test.go
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"errors"
+ "fmt"
"net/http"
"net/http/httptest"
"os"
@@ -975,3 +976,142 @@ func TestListCharacters(t *testing.T) {
assert.Equal(t, 0, len(response.SelfOwned), "Should return empty list for userID 0")
})
}
+
+func TestGetDatasheetOptions(t *testing.T) {
+ // Setup test environment
+ original := os.Getenv("ENVIRONMENT")
+ os.Setenv("ENVIRONMENT", "test")
+ t.Cleanup(func() {
+ if original != "" {
+ os.Setenv("ENVIRONMENT", original)
+ } else {
+ os.Unsetenv("ENVIRONMENT")
+ }
+ })
+
+ // Setup test database
+ database.SetupTestDB(true, true)
+ defer database.ResetTestDB()
+
+ err := models.MigrateStructure()
+ assert.NoError(t, err)
+
+ /*
+ // Populate misc lookup data
+ err = models.PopulateMiscLookupData()
+ assert.NoError(t, err)
+ */
+
+ // Create test character with weapon skill
+ testChar := &models.Char{
+ BamortBase: models.BamortBase{
+ Name: "Test Character",
+ },
+ Typ: "Krieger",
+ Rasse: "Mensch",
+ Waffenfertigkeiten: []models.SkWaffenfertigkeit{
+ {
+ SkFertigkeit: models.SkFertigkeit{
+ BamortCharTrait: models.BamortCharTrait{
+ BamortBase: models.BamortBase{
+ Name: "Langschwert",
+ },
+ },
+ },
+ },
+ },
+ }
+ err = testChar.Create()
+ assert.NoError(t, err)
+ assert.NotZero(t, testChar.ID, "Character ID should be set after Create")
+
+ // Setup Gin context
+ gin.SetMode(gin.TestMode)
+ w := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(w)
+ // Use string conversion of actual character ID
+ c.Params = gin.Params{{Key: "id", Value: fmt.Sprintf("%d", testChar.ID)}}
+
+ // Call the handler
+ GetDatasheetOptions(c)
+
+ // Assert response
+ assert.Equal(t, http.StatusOK, w.Code)
+
+ var response map[string]interface{}
+ err = json.Unmarshal(w.Body.Bytes(), &response)
+ assert.NoError(t, err)
+
+ // Verify all expected keys exist
+ assert.Contains(t, response, "gender")
+ assert.Contains(t, response, "races")
+ assert.Contains(t, response, "origins")
+ assert.Contains(t, response, "social_classes")
+ assert.Contains(t, response, "faiths")
+ assert.Contains(t, response, "handedness")
+ assert.Contains(t, response, "specializations")
+
+ // Verify data from database
+ genders := response["gender"].([]interface{})
+ assert.Equal(t, 3, len(genders))
+ assert.Contains(t, genders, "divers")
+ assert.Contains(t, genders, "männlich")
+ assert.Contains(t, genders, "weiblich")
+
+ races := response["races"].([]interface{})
+ assert.Equal(t, 5, len(races))
+ assert.Contains(t, races, "Elf")
+ assert.Contains(t, races, "Mensch")
+
+ origins := response["origins"].([]interface{})
+ assert.Equal(t, 15, len(origins))
+ assert.Contains(t, origins, "Albai")
+
+ socialClasses := response["social_classes"].([]interface{})
+ assert.Equal(t, 3, len(socialClasses))
+ assert.Contains(t, socialClasses, "Adel")
+ assert.Contains(t, socialClasses, "Mittelschicht")
+
+ faiths := response["faiths"].([]interface{})
+ assert.Equal(t, 5, len(faiths))
+ assert.Contains(t, faiths, "Druide")
+ assert.Contains(t, faiths, "Keine")
+
+ handedness := response["handedness"].([]interface{})
+ assert.Equal(t, 3, len(handedness))
+ assert.Contains(t, handedness, "beidhändig")
+ assert.Contains(t, handedness, "links")
+ assert.Contains(t, handedness, "rechts")
+}
+
+func TestGetDatasheetOptions_CharacterNotFound(t *testing.T) {
+ // Setup test environment
+ original := os.Getenv("ENVIRONMENT")
+ os.Setenv("ENVIRONMENT", "test")
+ t.Cleanup(func() {
+ if original != "" {
+ os.Setenv("ENVIRONMENT", original)
+ } else {
+ os.Unsetenv("ENVIRONMENT")
+ }
+ })
+
+ // Setup test database
+ database.SetupTestDB(true, true)
+ defer database.ResetTestDB()
+
+ err := models.MigrateStructure()
+ assert.NoError(t, err)
+
+ // Setup Gin context with non-existent character ID
+ gin.SetMode(gin.TestMode)
+ w := httptest.NewRecorder()
+ c, _ := gin.CreateTestContext(w)
+ c.Params = gin.Params{{Key: "id", Value: "99999"}}
+
+ // Call the handler
+ GetDatasheetOptions(c)
+
+ // Assert error response
+ assert.Equal(t, http.StatusNotFound, w.Code)
+}
diff --git a/backend/character/routes.go b/backend/character/routes.go
index 825f814..bc39338 100644
--- a/backend/character/routes.go
+++ b/backend/character/routes.go
@@ -12,6 +12,7 @@ func RegisterRoutes(r *gin.RouterGroup) {
charGrp.PUT("/:id", UpdateCharacter)
charGrp.DELETE("/:id", DeleteCharacter)
charGrp.PUT("/:id/image", UpdateCharacterImage)
+ charGrp.GET("/:id/datasheet-options", GetDatasheetOptions)
// Erfahrung und Vermögen
charGrp.GET("/:id/experience-wealth", GetCharacterExperienceAndWealth) // NewSystem
diff --git a/backend/cmd/main.go b/backend/cmd/main.go
index fe775e3..4e1f016 100644
--- a/backend/cmd/main.go
+++ b/backend/cmd/main.go
@@ -59,6 +59,16 @@ func main() {
database.ConnectDatabase()
logger.Info("Datenbankverbindung erfolgreich")
+ /*
+ // Populate initial misc lookup data
+ logger.Debug("Initialisiere Misc-Lookup-Daten...")
+ if err := models.PopulateMiscLookupData(); err != nil {
+ logger.Warn("Fehler beim Initialisieren der Misc-Lookup-Daten: %s", err.Error())
+ } else {
+ logger.Info("Misc-Lookup-Daten erfolgreich initialisiert")
+ }
+ */
+
// Initialize PDF templates
logger.Debug("Initialisiere PDF-Templates...")
if err := pdfrender.InitializeTemplates("/app/default_templates", cfg.TemplatesDir); err != nil {
diff --git a/backend/gsmaster/misclookup.go b/backend/gsmaster/misclookup.go
new file mode 100644
index 0000000..b7fb582
--- /dev/null
+++ b/backend/gsmaster/misclookup.go
@@ -0,0 +1,77 @@
+package gsmaster
+
+import (
+ "bamort/database"
+ "bamort/models"
+)
+
+// GetMiscLookupByKey retrieves all values for a given key
+func GetMiscLookupByKey(key string) ([]models.MiscLookup, error) {
+ var items []models.MiscLookup
+ err := database.DB.Where("`key` = ?", key).Order("value ASC").Find(&items).Error
+ return items, err
+}
+
+/*
+// PopulateMiscLookupData populates initial misc lookup data if table is empty
+func PopulateMiscLookupData() error {
+ // Check if data already exists
+ var count int64
+ if err := database.DB.Model(&models.MiscLookup{}).Count(&count).Error; err != nil {
+ return err
+ }
+ if count > 0 {
+ return nil // Data already exists
+ }
+
+ // Define initial data
+ initialData := []struct {
+ key string
+ values []string
+ }{
+ {
+ key: "gender",
+ values: []string{"männlich", "weiblich", "divers"},
+ },
+ {
+ key: "races",
+ values: []string{"Mensch", "Elf", "Zwerg", "Gnom", "Halbling"},
+ },
+ {
+ key: "origins",
+ values: []string{
+ "Albai", "Aran", "Chryseia", "Clanngadarn", "Erainn",
+ "Eschar", "Fuardain", "Ikengabecken", "KanThaiPan", "Küstenstaaten",
+ "Moravod", "Nahuatlan", "Rawindra", "Twyneddin", "Valian",
+ },
+ },
+ {
+ key: "social_classes",
+ values: []string{"Volk", "Mittelschicht", "Adel"},
+ },
+ {
+ key: "faiths",
+ values: []string{"Keine", "Nathir", "Deis Albai", "Mahal", "Druide"},
+ },
+ {
+ key: "handedness",
+ values: []string{"rechts", "links", "beidhändig"},
+ },
+ }
+
+ // Insert data
+ for _, item := range initialData {
+ for _, value := range item.values {
+ misc := models.MiscLookup{
+ Key: item.key,
+ Value: value,
+ }
+ if err := database.DB.Create(&misc).Error; err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+*/
diff --git a/backend/gsmaster/misclookup_test.go b/backend/gsmaster/misclookup_test.go
new file mode 100644
index 0000000..86d0249
--- /dev/null
+++ b/backend/gsmaster/misclookup_test.go
@@ -0,0 +1,123 @@
+package gsmaster
+
+import (
+ "bamort/database"
+ "bamort/models"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestMiscLookup_TableName(t *testing.T) {
+ misc := models.MiscLookup{}
+ assert.Equal(t, "gsm_misc", misc.TableName())
+}
+
+func TestMiscLookup_CreateAndRetrieve(t *testing.T) {
+ database.SetupTestDB()
+
+ // Create test data
+ testData := []models.MiscLookup{
+ {Key: "gender", Value: "männlich"},
+ {Key: "gender", Value: "weiblich"},
+ {Key: "races", Value: "Mensch"},
+ {Key: "races", Value: "Elf"},
+ }
+
+ // Insert test data
+ for _, item := range testData {
+ err := database.DB.Create(&item).Error
+ require.NoError(t, err)
+ }
+
+ // Retrieve by key
+ genders, err := GetMiscLookupByKey("gender")
+ require.NoError(t, err)
+ assert.Len(t, genders, 2)
+ assert.Equal(t, "männlich", genders[0].Value)
+ assert.Equal(t, "weiblich", genders[1].Value)
+
+ races, err := GetMiscLookupByKey("races")
+ require.NoError(t, err)
+ assert.Len(t, races, 2)
+}
+
+func TestGetMiscLookupByKey_NotFound(t *testing.T) {
+ database.SetupTestDB()
+
+ items, err := GetMiscLookupByKey("nonexistent")
+ require.NoError(t, err)
+ assert.Empty(t, items)
+}
+
+func TestMiscLookup_WithSourceInfo(t *testing.T) {
+ database.SetupTestDB()
+
+ misc := models.MiscLookup{
+ Key: "test_key",
+ Value: "test_value",
+ SourceID: 1,
+ PageNumber: 42,
+ }
+
+ err := database.DB.Create(&misc).Error
+ require.NoError(t, err)
+ assert.NotZero(t, misc.ID)
+
+ // Retrieve and verify
+ var retrieved models.MiscLookup
+ err = database.DB.First(&retrieved, misc.ID).Error
+ require.NoError(t, err)
+ assert.Equal(t, "test_key", retrieved.Key)
+ assert.Equal(t, "test_value", retrieved.Value)
+ assert.Equal(t, uint(1), retrieved.SourceID)
+ assert.Equal(t, 42, retrieved.PageNumber)
+}
+
+/*
+func TestPopulateMiscLookupData(t *testing.T) {
+ database.SetupTestDB()
+
+ // First population should succeed
+ err := PopulateMiscLookupData()
+ require.NoError(t, err)
+
+ // Verify all keys have data
+ expectedCounts := map[string]int{
+ "gender": 3,
+ "races": 5,
+ "origins": 15,
+ "social_classes": 3,
+ "faiths": 5,
+ "handedness": 3,
+ }
+
+ for key, expectedCount := range expectedCounts {
+ items, err := GetMiscLookupByKey(key)
+ require.NoError(t, err)
+ assert.Len(t, items, expectedCount, "Expected %d items for key %s", expectedCount, key)
+ }
+
+ // Verify specific values
+ genders, _ := GetMiscLookupByKey("gender")
+ assert.Contains(t, []string{"männlich", "weiblich", "divers"}, genders[0].Value)
+
+ races, _ := GetMiscLookupByKey("races")
+ raceValues := make([]string, len(races))
+ for i, r := range races {
+ raceValues[i] = r.Value
+ }
+ assert.Contains(t, raceValues, "Mensch")
+ assert.Contains(t, raceValues, "Elf")
+
+ // Second population should not duplicate data
+ err = PopulateMiscLookupData()
+ require.NoError(t, err)
+
+ var totalCount int64
+ err = database.DB.Model(&MiscLookup{}).Count(&totalCount).Error
+ require.NoError(t, err)
+ assert.Equal(t, int64(34), totalCount, "Should not duplicate data on second population")
+}
+*/
diff --git a/backend/models/database.go b/backend/models/database.go
index 4921d07..caabb2b 100644
--- a/backend/models/database.go
+++ b/backend/models/database.go
@@ -61,6 +61,7 @@ func gsMasterMigrateStructure(db ...*gorm.DB) error {
&Container{},
&Transportation{},
&Believe{},
+ &MiscLookup{},
)
if err != nil {
return err
diff --git a/backend/models/model_gsmaster.go b/backend/models/model_gsmaster.go
index 577e2bc..09605f5 100644
--- a/backend/models/model_gsmaster.go
+++ b/backend/models/model_gsmaster.go
@@ -128,6 +128,15 @@ type Believe struct {
PageNumber int `json:"page_number,omitempty"` // Seitenzahl im Quellenbuch
}
+// MiscLookup represents miscellaneous lookup values like gender, race, origin, etc.
+type MiscLookup struct {
+ ID uint `gorm:"primaryKey" json:"id"`
+ Key string `gorm:"column:key;type:varchar(50);index;not null" json:"key"`
+ Value string `gorm:"type:varchar(255);not null" json:"value"`
+ SourceID uint `json:"source_id,omitempty"`
+ PageNumber int `json:"page_number,omitempty"`
+}
+
func (object *Skill) TableName() string {
dbPrefix := "gsm"
return dbPrefix + "_" + "skills"
@@ -669,3 +678,8 @@ func GetBelievesByActiveSources(gameSystem string) ([]Believe, error) {
Find(&believes).Error
return believes, err
}
+
+// TableName specifies the table name for MiscLookup
+func (MiscLookup) TableName() string {
+ return "gsm_misc"
+}
diff --git a/frontend/src/components/DatasheetView.vue b/frontend/src/components/DatasheetView.vue
index 5b028c1..dbff9ed 100644
--- a/frontend/src/components/DatasheetView.vue
+++ b/frontend/src/components/DatasheetView.vue
@@ -49,7 +49,9 @@
@dblclick="startEditProp('gender', character.gender)"
class="editable-prop"
>{{ character.gender || 'x' }}
-
+
),
Grad:
{{ character.rasse || 'x' }}
- ,
+ ,
Heimat:
{{ character.origin || '-' }}
- ,
+ ,
Stand:
{{ character.social_class || '-' }}
- .
+ .
Hort für Grad {{ character.grad || 'x' }}: 125 GS, für nächsten Grad: 250 GS.
@@ -90,7 +98,10 @@
@dblclick="startEditProp('spezialisierung', character.spezialisierung)"
class="editable-prop"
>{{ character.spezialisierung || '-' }}
- .
+ .
Alter:
@@ -105,7 +116,9 @@
Linkshänder
Beidhändig
- ,
+ ,
Größe:
{{ character.glaube || '-' }}
-
+
Merkmale:
@@ -208,6 +223,7 @@ export default {
editingProp: null,
editPropValue: '',
editPropType: 'text',
+ datasheetOptions: null,
characterStats: [
{ label: 'stats.strength', path: 'eigenschaften.6.value' },
{ label: 'stats.dexterity', path: 'eigenschaften.1.value' },
@@ -228,6 +244,17 @@ export default {
}
},
methods: {
+ async loadDatasheetOptions() {
+ if (this.datasheetOptions) return
+
+ try {
+ const response = await API.get(`/api/characters/${this.character.id}/datasheet-options`)
+ this.datasheetOptions = response.data
+ } catch (error) {
+ console.error('Failed to load datasheet options:', error)
+ alert('Fehler beim Laden der Auswahloptionen')
+ }
+ },
handleImageUpdate(newImage) {
this.$emit('character-updated')
},
@@ -283,6 +310,13 @@ export default {
this.editValue = ''
},
startEditProp(prop, value, type = 'text') {
+ // Load options if this is a select field
+ const selectFields = ['gender', 'rasse', 'origin', 'social_class', 'glaube', 'hand', 'spezialisierung']
+ if (selectFields.includes(prop)) {
+ this.loadDatasheetOptions()
+ type = 'select'
+ }
+
this.editingProp = prop
this.editPropValue = value || ''
this.editPropType = type
@@ -291,7 +325,9 @@ export default {
const input = Array.isArray(this.$refs.propInput) ? this.$refs.propInput[0] : this.$refs.propInput
if (input) {
input.focus()
- input.select()
+ if (type !== 'select') {
+ input.select()
+ }
}
}
})
@@ -331,6 +367,21 @@ export default {
this.cancelEditProp()
}
},
+ getSelectOptions(prop) {
+ if (!this.datasheetOptions) return []
+
+ const optionMap = {
+ 'gender': 'gender',
+ 'rasse': 'races',
+ 'origin': 'origins',
+ 'social_class': 'social_classes',
+ 'glaube': 'faiths',
+ 'hand': 'handedness',
+ 'spezialisierung': 'specializations'
+ }
+
+ return this.datasheetOptions[optionMap[prop]] || []
+ },
cancelEditProp() {
this.editingProp = null
this.editPropValue = ''