Skill editieren un neu erstellen

This commit is contained in:
2026-01-04 17:49:04 +01:00
parent fe72f794a3
commit fcf785081c
7 changed files with 503 additions and 5 deletions
+2 -1
View File
@@ -12,7 +12,8 @@ func RegisterRoutes(r *gin.RouterGroup) {
{
maintGrp.GET("", GetMasterData)
maintGrp.GET("/skills", GetMDSkills)
maintGrp.GET("/skills-enhanced", GetEnhancedMDSkills) // New enhanced endpoint
maintGrp.GET("/skills-enhanced", GetEnhancedMDSkills) // New enhanced endpoint
maintGrp.POST("/skills-enhanced", CreateEnhancedMDSkill) // Create new skill
maintGrp.GET("/skills/:id", GetMDSkill)
maintGrp.GET("/skills-enhanced/:id", GetEnhancedMDSkill) // New enhanced endpoint
maintGrp.PUT("/skills/:id", UpdateMDSkill)
+149
View File
@@ -0,0 +1,149 @@
package gsmaster
import (
"bamort/database"
"bamort/models"
"testing"
)
func TestCreateSkillWithCategories(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Create test dependencies
source := getOrCreateSource("TSTCRT", "TestCreate")
category := getOrCreateCategory("Alltag", source.ID)
difficulty := getOrCreateDifficulty("normal")
// Prepare create request
req := SkillUpdateRequest{
Skill: models.Skill{
Name: "Neue Fertigkeit",
GameSystem: "midgard",
Beschreibung: "Test Fertigkeit",
Initialwert: 5,
BasisWert: 0,
Bonuseigenschaft: "In",
Improvable: true,
InnateSkill: false,
SourceID: source.ID,
PageNumber: 42,
},
CategoryDifficulties: []CategoryDifficultyPair{
{
CategoryID: category.ID,
DifficultyID: difficulty.ID,
},
},
}
// Test creating new skill
skillID, err := CreateSkillWithCategories(req)
if err != nil {
t.Fatalf("CreateSkillWithCategories failed: %v", err)
}
if skillID == 0 {
t.Fatalf("Expected non-zero skill ID, got 0")
}
// Verify skill was created
var skill models.Skill
if err := database.DB.First(&skill, skillID).Error; err != nil {
t.Fatalf("Failed to retrieve created skill: %v", err)
}
if skill.Name != "Neue Fertigkeit" {
t.Errorf("Expected name 'Neue Fertigkeit', got '%s'", skill.Name)
}
if skill.Initialwert != 5 {
t.Errorf("Expected initialwert 5, got %d", skill.Initialwert)
}
if skill.BasisWert != 0 {
t.Errorf("Expected basiswert 0, got %d", skill.BasisWert)
}
// Verify category-difficulty relationship
var scd models.SkillCategoryDifficulty
if err := database.DB.Where("skill_id = ?", skillID).First(&scd).Error; err != nil {
t.Fatalf("Failed to retrieve skill category difficulty: %v", err)
}
if scd.SkillCategoryID != category.ID {
t.Errorf("Expected category ID %d, got %d", category.ID, scd.SkillCategoryID)
}
if scd.SkillDifficultyID != difficulty.ID {
t.Errorf("Expected difficulty ID %d, got %d", difficulty.ID, scd.SkillDifficultyID)
}
}
func TestCreateSkillWithMultipleCategories(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Create test dependencies
source := getOrCreateSource("TSTMLT", "TestMultiple")
category1 := getOrCreateCategory("Körper", source.ID)
category2 := getOrCreateCategory("Geist", source.ID)
difficulty1 := getOrCreateDifficulty("leicht")
difficulty2 := getOrCreateDifficulty("schwer")
// Prepare create request with multiple categories
req := SkillUpdateRequest{
Skill: models.Skill{
Name: "Multi-Kategorie Fertigkeit",
GameSystem: "midgard",
Initialwert: 10,
Improvable: true,
SourceID: source.ID,
},
CategoryDifficulties: []CategoryDifficultyPair{
{
CategoryID: category1.ID,
DifficultyID: difficulty1.ID,
},
{
CategoryID: category2.ID,
DifficultyID: difficulty2.ID,
},
},
}
// Test creating skill with multiple categories
skillID, err := CreateSkillWithCategories(req)
if err != nil {
t.Fatalf("CreateSkillWithCategories failed: %v", err)
}
// Verify both category-difficulty relationships exist
var scds []models.SkillCategoryDifficulty
if err := database.DB.Where("skill_id = ?", skillID).Find(&scds).Error; err != nil {
t.Fatalf("Failed to retrieve skill category difficulties: %v", err)
}
if len(scds) != 2 {
t.Fatalf("Expected 2 category-difficulty relationships, got %d", len(scds))
}
}
func TestCreateSkillValidation(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Test creating skill without name
req := SkillUpdateRequest{
Skill: models.Skill{
GameSystem: "midgard",
Initialwert: 5,
},
CategoryDifficulties: []CategoryDifficultyPair{},
}
_, err := CreateSkillWithCategories(req)
if err == nil {
t.Error("Expected error when creating skill without name, got nil")
}
}
+92 -2
View File
@@ -95,12 +95,77 @@ type CategoryDifficultyPair struct {
LearnCost int `json:"learn_cost,omitempty"`
}
// CreateSkillWithCategories creates a new skill with category-difficulty relationships
func CreateSkillWithCategories(req SkillUpdateRequest) (uint, error) {
// Validate required fields
if req.Skill.Name == "" {
return 0, fmt.Errorf("skill name is required")
}
var skillID uint
// Start transaction
err := database.DB.Transaction(func(tx *gorm.DB) error {
// Create skill
if err := tx.Create(&req.Skill).Error; err != nil {
return err
}
skillID = req.Skill.ID
// Create category-difficulty relationships
for _, cd := range req.CategoryDifficulties {
// Get category and difficulty names for denormalized fields
var category models.SkillCategory
if err := tx.First(&category, cd.CategoryID).Error; err != nil {
return fmt.Errorf("category not found: %w", err)
}
var difficulty models.SkillDifficulty
if err := tx.First(&difficulty, cd.DifficultyID).Error; err != nil {
return fmt.Errorf("difficulty not found: %w", err)
}
learnCost := cd.LearnCost
if learnCost == 0 {
// Use default based on difficulty
learnCost = getDefaultLearnCost(difficulty.Name)
}
scd := models.SkillCategoryDifficulty{
SkillID: skillID,
SkillCategoryID: cd.CategoryID,
SkillDifficultyID: cd.DifficultyID,
LearnCost: learnCost,
SCategory: category.Name,
SDifficulty: difficulty.Name,
}
if err := tx.Create(&scd).Error; err != nil {
return err
}
}
return nil
})
if err != nil {
return 0, err
}
return skillID, nil
}
// UpdateSkillWithCategories updates a skill and its category-difficulty relationships
func UpdateSkillWithCategories(skillID uint, req SkillUpdateRequest) error {
// Start transaction
return database.DB.Transaction(func(tx *gorm.DB) error {
// Update skill basic info
if err := tx.Model(&models.Skill{}).Where("id = ?", skillID).Updates(req.Skill).Error; err != nil {
// Update skill basic info - use Select to explicitly include boolean fields
// This ensures false values are also updated (GORM skips zero values by default in Updates)
if err := tx.Model(&models.Skill{}).Where("id = ?", skillID).
Select("name", "beschreibung", "game_system", "initialwert", "basis_wert",
"bonuseigenschaft", "improvable", "innate_skill", "source_id", "page_number").
Updates(req.Skill).Error; err != nil {
return err
}
@@ -240,3 +305,28 @@ func UpdateEnhancedMDSkill(c *gin.Context) {
c.JSON(http.StatusOK, skill)
}
// CreateEnhancedMDSkill creates a new skill with categories
func CreateEnhancedMDSkill(c *gin.Context) {
var req SkillUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondWithError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
// Create the skill
skillID, err := CreateSkillWithCategories(req)
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to create skill: "+err.Error())
return
}
// Return created skill
skill, err := GetSkillWithCategories(skillID)
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve created skill")
return
}
c.JSON(http.StatusCreated, skill)
}
@@ -292,6 +292,73 @@ func TestUpdateSkillWithCategories(t *testing.T) {
t.Error("Expected to find 'Alltag/normal' category after update")
}
}
func TestUpdateSkillBooleanFields(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Create test data with improvable=true and innateskill=false
source := getOrCreateSource("TSTBOOL", "TestBoolean")
skill := models.Skill{
Name: "TestBooleanSkill",
GameSystem: "midgard",
Initialwert: 5,
Improvable: true,
InnateSkill: false,
SourceID: source.ID,
}
database.DB.Create(&skill)
category := getOrCreateCategory("Alltag", source.ID)
difficulty := getOrCreateDifficulty("normal")
scd := models.SkillCategoryDifficulty{
SkillID: skill.ID,
SkillCategoryID: category.ID,
SkillDifficultyID: difficulty.ID,
LearnCost: 10,
SCategory: category.Name,
SDifficulty: difficulty.Name,
}
database.DB.Create(&scd)
// Update to set improvable=false and innateskill=true
updateReq := SkillUpdateRequest{
Skill: models.Skill{
ID: skill.ID,
Name: "TestBooleanSkill",
GameSystem: "midgard",
Initialwert: 5,
Improvable: false, // Change to false
InnateSkill: true, // Change to true
SourceID: source.ID,
},
CategoryDifficulties: []CategoryDifficultyPair{
{
CategoryID: category.ID,
DifficultyID: difficulty.ID,
},
},
}
err := UpdateSkillWithCategories(skill.ID, updateReq)
if err != nil {
t.Fatalf("UpdateSkillWithCategories failed: %v", err)
}
// Verify boolean fields were updated correctly
var updatedSkill models.Skill
if err := database.DB.First(&updatedSkill, skill.ID).Error; err != nil {
t.Fatalf("Failed to retrieve updated skill: %v", err)
}
if updatedSkill.Improvable != false {
t.Errorf("Expected improvable to be false, got %v", updatedSkill.Improvable)
}
if updatedSkill.InnateSkill != true {
t.Errorf("Expected innateskill to be true, got %v", updatedSkill.InnateSkill)
}
}
func TestGetDefaultLearnCost(t *testing.T) {
tests := []struct {
@@ -7,6 +7,7 @@
v-model="searchTerm"
:placeholder="`${$t('search')} ${$t('Skill')}...`"
/>
<button @click="startCreate" class="btn-primary">{{ $t('newSkill') }}</button>
</div>
</div>
@@ -80,6 +81,117 @@
</tr>
</thead>
<tbody>
<!-- Create New Skill Row -->
<tr v-if="creatingNew">
<td>New</td>
<td colspan="11">
<!-- Create form -->
<div class="edit-form">
<div class="edit-row">
<div class="edit-field">
<label>{{ $t('skill.name') }}:</label>
<input v-model="editedItem.name" />
</div>
<div class="edit-field">
<label>{{ $t('skill.initialwert') }}:</label>
<input v-model.number="editedItem.initialwert" type="number" style="width:60px;" />
</div>
<div class="edit-field">
<label>{{ $t('skill.basiswert') }}:</label>
<input v-model.number="editedItem.basiswert" type="number" style="width:60px;" />
</div>
<div class="edit-field">
<label>{{ $t('skill.bonusskill') }}:</label>
<select v-model="editedItem.bonuseigenschaft" style="width:80px;">
<option value="">-</option>
<option value="St">St</option>
<option value="Gs">Gs</option>
<option value="Gw">Gw</option>
<option value="Ko">Ko</option>
<option value="In">In</option>
<option value="Zt">Zt</option>
<option value="Au">Au</option>
<option value="pA">pA</option>
<option value="Wk">Wk</option>
<option value="B">B</option>
</select>
</div>
</div>
<div class="edit-row">
<div class="edit-field">
<label>{{ $t('skill.improvable') }}:</label>
<input type="checkbox" v-model="editedItem.improvable" />
</div>
<div class="edit-field">
<label>{{ $t('skill.innateskill') }}:</label>
<input type="checkbox" v-model="editedItem.innateskill" />
</div>
</div>
<div class="edit-row">
<div class="edit-field full-width">
<label>{{ $t('skill.description') }}:</label>
<input v-model="editedItem.beschreibung" />
</div>
</div>
<div class="edit-row">
<div class="edit-field">
<label>{{ $t('skill.quelle') }}:</label>
<select v-model="editedItem.sourceCode" style="width:100px;">
<option v-for="source in availableSources" :key="source.code" :value="source.code">
{{ source.code }}
</option>
</select>
</div>
<div class="edit-field">
<label>{{ $t('skill.page') || 'Page' }}:</label>
<input v-model.number="editedItem.page_number" type="number" style="width:60px;" />
</div>
</div>
<div class="edit-row">
<div class="edit-field full-width">
<label>{{ $t('skill.categories') || 'Categories' }}:</label>
<div class="category-checkboxes">
<div v-for="category in mdata.categories" :key="category.id" class="category-checkbox">
<input
type="checkbox"
:value="category.id"
v-model="editedItem.selectedCategories"
@change="onCategoryToggle(category.id)"
/>
<label>{{ category.name }}</label>
</div>
</div>
</div>
</div>
<div class="edit-row">
<div class="edit-field full-width">
<label>{{ $t('skill.difficulty') || 'Difficulty' }}:</label>
<div class="difficulty-selects">
<div v-for="catId in editedItem.selectedCategories" :key="catId" class="difficulty-select">
<span>{{ getCategoryName(catId) }}:</span>
<select v-model="editedItem.categoryDifficulties[catId]" style="width:120px;">
<option v-for="diff in mdata.difficulties" :key="diff.id" :value="diff.id">
{{ diff.name }}
</option>
</select>
</div>
</div>
</div>
</div>
<div class="edit-actions">
<button @click="saveCreate" class="btn-save">{{ $t('createSkill') }}</button>
<button @click="cancelCreate" class="btn-cancel">Cancel</button>
</div>
</div>
</td>
</tr>
<template v-for="(dtaItem, index) in filteredAndSortedSkills" :key="dtaItem.id">
<!-- Display Mode -->
<tr v-if="editingIndex !== index">
@@ -189,7 +301,7 @@
<div class="edit-row">
<div class="edit-field full-width">
<label>{{ $t('skill.difficulties') || 'Difficulties' }}:</label>
<label>{{ $t('skill.difficulty') || 'Difficulty' }}:</label>
<div class="difficulty-selects">
<div v-for="catId in editedItem.selectedCategories" :key="catId" class="difficulty-select">
<span>{{ getCategoryName(catId) }}:</span>
@@ -246,6 +358,7 @@ export default {
sortAsc: true,
editingIndex: -1,
editedItem: null,
creatingNew: false,
filterCategory: '',
filterDifficulty: '',
filterImprovable: '',
@@ -492,6 +605,76 @@ export default {
this.filterImprovable = ''
this.filterInnateskill = ''
this.filterBonuseigenschaft = ''
},
startCreate() {
// Initialize new skill object with defaults
this.editedItem = {
name: '',
beschreibung: '',
game_system: 'midgard',
initialwert: 5,
basiswert: 0,
bonuseigenschaft: '',
improvable: true,
innateskill: false,
sourceCode: this.availableSources.length > 0 ? this.availableSources[0].code : '',
page_number: 0,
selectedCategories: [],
categoryDifficulties: {}
}
this.creatingNew = true
},
async saveCreate() {
try {
// Validate required fields
if (!this.editedItem.name) {
alert('Skill name is required')
return
}
// Find source ID from code
const source = this.availableSources.find(s => s.code === this.editedItem.sourceCode)
// Build category_difficulties array
const categoryDifficulties = this.editedItem.selectedCategories.map(catId => ({
category_id: catId,
difficulty_id: this.editedItem.categoryDifficulties[catId]
}))
const createData = {
name: this.editedItem.name,
beschreibung: this.editedItem.beschreibung,
game_system: this.editedItem.game_system || 'midgard',
initialwert: this.editedItem.initialwert,
basiswert: this.editedItem.basiswert || 0,
bonuseigenschaft: this.editedItem.bonuseigenschaft,
improvable: this.editedItem.improvable,
innateskill: this.editedItem.innateskill,
source_id: source ? source.id : null,
page_number: this.editedItem.page_number || 0,
category_difficulties: categoryDifficulties
}
const response = await API.post(
'/api/maintenance/skills-enhanced',
createData
)
// Add the new skill to the list
this.enhancedSkills.push(response.data)
// Hide the create dialog
this.creatingNew = false
this.editedItem = null
} catch (error) {
console.error('Failed to create skill:', error)
alert('Failed to create skill: ' + (error.response?.data?.error || error.message))
// Don't close dialog on error so user can fix the issue
}
},
cancelCreate() {
this.creatingNew = false
this.editedItem = null
}
}
}
+4
View File
@@ -109,6 +109,8 @@ export default {
initialwert:'Startwert',
basiswert:'Basiswert (ungelernt)',
difficulty:'Schwierigkeit',
page:'Seite',
categories:'Kategorien',
},
spell:{
id:'ID',
@@ -233,6 +235,8 @@ export default {
},
search:'Suche',
Skill:'Fertigkeit',
newSkill:'Neue Fertigkeit',
createSkill:'Fertigkeit erstellen',
common: {
loading: 'Laden...',
cancel: 'Abbrechen',
+5 -1
View File
@@ -105,6 +105,8 @@ export default {
category:'Category',
initialwert:'Initial value',
basiswert:'Base value (untrained)',
page:'Page',
categories:'Categories',
},
spell:{
id:'ID',
@@ -125,7 +127,7 @@ export default {
quelle:'Source',
import: 'Import',
selectCsv: 'select CSV',
system: 'System'
system: 'System',
},
spells: {
learn: {
@@ -229,6 +231,8 @@ export default {
},
search:'Suche',
Skill:'Fertigkeit',
newSkill:'New Skill',
createSkill:'Create Skill',
common: {
loading: 'Laden...',
cancel: 'Abbrechen',