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 = ''