Compare commits

...

2 Commits

Author SHA1 Message Date
Frank 09d12d3d8c extra learn_categories added to spell maintenance 2026-02-27 11:59:16 +01:00
Bardioc26 6ac04b2ae1 Char skill edit not saved (#36)
* Skill edit was not saved
* show warning when skill name is to be edited in char_skills
2026-02-27 11:59:16 +01:00
10 changed files with 337 additions and 14 deletions
+11 -1
View File
@@ -147,7 +147,17 @@ func UpdateCharacter(c *gin.Context) {
return
}
c.JSON(http.StatusOK, character)
// Reload character to get updated data
var updatedCharacter models.Char
err = updatedCharacter.FirstID(id)
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to reload character")
return
}
// Return as FeChar with categorized skills
feChar := ToFeChar(&updatedCharacter)
c.JSON(http.StatusOK, feChar)
}
func DeleteCharacter(c *gin.Context) {
id := c.Param("id")
+1
View File
@@ -47,6 +47,7 @@ func RegisterRoutes(r *gin.RouterGroup) {
maintGrp.PUT("/spells/:id", UpdateMDSpell)
maintGrp.PUT("/spells-enhanced/:id", UpdateEnhancedMDSpell) // New enhanced endpoint
maintGrp.POST("/spells", AddSpell)
maintGrp.POST("/spells-enhanced", CreateEnhancedMDSpell) // New enhanced endpoint
maintGrp.DELETE("/spells/:id", DeleteMDSpell)
maintGrp.PUT("/equipment/:id", UpdateMDEquipment)
+43 -3
View File
@@ -66,6 +66,15 @@ func UpdateSpellWithCategories(spellID uint, req SpellUpdateRequest) error {
})
}
// CreateSpellWithCategories creates a new spell
func CreateSpellWithCategories(req SpellUpdateRequest) (*models.Spell, error) {
spell := req.Spell
if err := database.DB.Create(&spell).Error; err != nil {
return nil, err
}
return &spell, nil
}
// ===== Handler Functions =====
// GetEnhancedMDSpells returns spells with enhanced information
@@ -87,11 +96,18 @@ func GetEnhancedMDSpells(c *gin.Context) {
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve spell categories: "+err.Error())
return
}
// Get spell Learn_categories
learnCategories, err := spell.GetSpellLearnCategories()
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve spell learn categories: "+err.Error())
return
}
c.JSON(http.StatusOK, gin.H{
"spells": spells,
"sources": sources,
"categories": categories,
"spells": spells,
"sources": sources,
"categories": categories,
"learnCategories": learnCategories,
})
}
@@ -145,3 +161,27 @@ func UpdateEnhancedMDSpell(c *gin.Context) {
c.JSON(http.StatusOK, spell)
}
// CreateEnhancedMDSpell creates a new spell
func CreateEnhancedMDSpell(c *gin.Context) {
var req SpellUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondWithError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
spell, err := CreateSpellWithCategories(req)
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to create spell: "+err.Error())
return
}
// Return created spell with enhanced information
spellWithCats, err := GetSpellWithCategories(spell.ID)
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve created spell")
return
}
c.JSON(http.StatusCreated, spellWithCats)
}
@@ -0,0 +1,153 @@
package gsmaster
import (
"bamort/database"
"bamort/models"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUpdateSpellWithLearningCategory(t *testing.T) {
// Set test environment
setupTestEnvironment(t)
database.SetupTestDB(true)
defer database.ResetTestDB()
// Create a test spell
spell := models.Spell{
Name: "Testzauber",
Category: "Wunder",
LearningCategory: "",
Stufe: 1,
GameSystem: "midgard",
}
err := database.DB.Create(&spell).Error
require.NoError(t, err, "Failed to create test spell")
// Test 1: Update learning_category
spell.LearningCategory = "Wundertat"
updateReq := SpellUpdateRequest{Spell: spell}
err = UpdateSpellWithCategories(spell.ID, updateReq)
assert.NoError(t, err, "Failed to update spell with learning_category")
// Verify update
var updated models.Spell
err = database.DB.First(&updated, spell.ID).Error
require.NoError(t, err)
assert.Equal(t, "Wundertat", updated.LearningCategory, "Learning category not updated correctly")
assert.Equal(t, "Wunder", updated.Category, "Category should remain unchanged")
// Test 2: Update both categories
updated.Category = "Verändern"
updated.LearningCategory = "Zauberlied"
updateReq2 := SpellUpdateRequest{Spell: updated}
err = UpdateSpellWithCategories(updated.ID, updateReq2)
assert.NoError(t, err, "Failed to update both categories")
// Verify both were updated
var final models.Spell
err = database.DB.First(&final, updated.ID).Error
require.NoError(t, err)
assert.Equal(t, "Verändern", final.Category, "Category not updated")
assert.Equal(t, "Zauberlied", final.LearningCategory, "Learning category not updated")
}
func TestCreateSpellWithLearningCategory(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB(true)
defer database.ResetTestDB()
// Test: Create spell with learning_category
spell := models.Spell{
Name: "Neuer Zauber",
Category: "Beherrschen",
LearningCategory: "Runenstab",
Stufe: 2,
GameSystem: "midgard",
AP: "3",
}
err := database.DB.Create(&spell).Error
require.NoError(t, err, "Failed to create spell with learning_category")
// Verify creation
var created models.Spell
err = database.DB.Where("name = ?", "Neuer Zauber").First(&created).Error
require.NoError(t, err)
assert.Equal(t, "Beherrschen", created.Category)
assert.Equal(t, "Runenstab", created.LearningCategory)
assert.Equal(t, 2, created.Stufe)
}
func TestLearningCategoryDefaultEmpty(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB(true)
defer database.ResetTestDB()
// Test: Create spell without learning_category
spell := models.Spell{
Name: "Zauber ohne LearningCategory",
Category: "Normal",
Stufe: 1,
GameSystem: "midgard",
}
err := database.DB.Create(&spell).Error
require.NoError(t, err)
// Verify learning_category is empty (not nil)
var created models.Spell
err = database.DB.First(&created, spell.ID).Error
require.NoError(t, err)
assert.Equal(t, "", created.LearningCategory, "Learning category should be empty string by default")
assert.Equal(t, "Normal", created.Category)
}
func TestCreateEnhancedMDSpellEndpoint(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB(true)
defer database.ResetTestDB()
// Test: Create spell with learning_category via endpoint
spell := models.Spell{
Name: "Test Endpoint Zauber",
Category: "Erkennen",
LearningCategory: "Erkenntnismagie",
Stufe: 3,
GameSystem: "midgard",
AP: "2",
}
req := SpellUpdateRequest{Spell: spell}
created, err := CreateSpellWithCategories(req)
require.NoError(t, err, "Failed to create spell")
assert.NotZero(t, created.ID, "Created spell should have an ID")
assert.Equal(t, "Erkennen", created.Category)
assert.Equal(t, "Erkenntnismagie", created.LearningCategory)
assert.Equal(t, 3, created.Stufe)
}
func TestGetSpellLearningCategories(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB(true)
defer database.ResetTestDB()
var spell models.Spell
learningCategories, err := spell.GetSpellLearnCategories()
require.NoError(t, err, "Failed to get spell learning categories")
assert.Contains(t, learningCategories, "Spruch", "Learning categories should include 'Spruch'")
assert.Contains(t, learningCategories, "Runenstab", "Learning categories should include 'Runenstab'")
assert.Contains(t, learningCategories, "Zauberlied", "Learning categories should include 'Zauberlied'")
assert.Contains(t, learningCategories, "Wundertat", "Learning categories should include 'Wundertat'")
assert.Contains(t, learningCategories, "Dweomer", "Learning categories should include 'Dweomer'")
assert.Contains(t, learningCategories, "Thaumatherapie", "Learning categories should include 'Thaumatherapie'")
assert.Contains(t, learningCategories, "Zaubersalz", "Learning categories should include 'Zaubersalz'")
assert.Contains(t, learningCategories, "Rune", "Learning categories should include 'Rune'")
assert.Contains(t, learningCategories, "Siegel", "Learning categories should include 'Siegel'")
}
+16
View File
@@ -438,6 +438,22 @@ func (object *Spell) GetSpellCategories() ([]string, error) {
return categories, nil
}
func (object *Spell) GetSpellLearnCategories() ([]string, error) {
var categories []string
gs := GetGameSystem(object.GameSystemId, object.GameSystem)
result := database.DB.Model(&Spell{}).
Where("game_system = ? OR game_system_id = ?", gs.Name, gs.ID).
Distinct().
Pluck("learning_category", &categories)
if result.Error != nil {
return nil, result.Error
}
return categories, nil
}
func (object *Spell) ensureGameSystem() {
gs := GetGameSystem(object.GameSystemId, object.GameSystem)
object.GameSystemId = gs.ID
+7 -1
View File
@@ -32,7 +32,7 @@
<!-- Submenu Content -->
<!-- <div class="character-aspect"> -->
<component :is="currentView" :character="character" :isOwner="isOwner" @character-updated="refreshCharacter"/>
<component :is="currentView" :character="character" :isOwner="isOwner" @character-updated="refreshCharacter" @character-data-updated="updateCharacterData"/>
<!-- </div> -->
<!-- Submenu -->
@@ -156,6 +156,12 @@ export default {
alert('Fehler beim Aktualisieren der Charakterdaten: ' + (error.response?.data?.error || error.message));
}
},
updateCharacterData(updatedData) {
// Update character data directly without reloading from server
this.character = updatedData;
console.log('Character data updated directly from response');
},
},
};
</script>
+75 -5
View File
@@ -55,6 +55,15 @@
</div>
</div>
</div>
<!-- Warning message when editing skill name -->
<div v-if="showNameEditWarning" class="warning-message">
<span class="warning-icon"></span>
<span class="warning-text">
{{ $t('characters.datasheet.editnamewarning') }}
</span>
</div>
<table class="cd-table">
<thead>
<tr>
@@ -384,6 +393,29 @@
transform: translateX(0);
}
}
.warning-message {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
margin: 10px 0;
background-color: #fff3cd;
border: 1px solid #ffc107;
border-radius: 6px;
color: #856404;
animation: slideIn 0.3s ease;
}
.warning-icon {
font-size: 1.2rem;
flex-shrink: 0;
}
.warning-text {
font-size: 0.9rem;
line-height: 1.4;
}
</style>
<script>
@@ -431,6 +463,7 @@ export default {
editingSkillId: null,
editingField: null,
editValue: '',
showNameEditWarning: false,
isLoading: false
};
@@ -752,6 +785,11 @@ export default {
this.editingField = field;
this.editValue = skill[field] || '';
// Show warning when editing skill name
if (field === 'name') {
this.showNameEditWarning = true;
}
this.$nextTick(() => {
const input = this.$refs.editInput;
if (input) {
@@ -779,19 +817,50 @@ export default {
}
}
// Update local character object
skill[field] = newValue;
// Find and update the original skill in character.fertigkeiten or character.waffenfertigkeiten
let originalSkillFound = false;
// Check in fertigkeiten
if (this.character.fertigkeiten) {
const originalSkill = this.character.fertigkeiten.find(s => s.name === skill.name);
if (originalSkill) {
originalSkill[field] = newValue;
originalSkillFound = true;
}
}
// Check in waffenfertigkeiten if not found in fertigkeiten
if (!originalSkillFound && this.character.waffenfertigkeiten) {
const originalWeaponSkill = this.character.waffenfertigkeiten.find(s => s.name === skill.name);
if (originalWeaponSkill) {
originalWeaponSkill[field] = newValue;
originalSkillFound = true;
}
}
if (!originalSkillFound) {
console.warn('Original skill not found in character data:', skill.name);
alert('Warnung: Originalfertigkeit nicht gefunden.');
this.cancelEditSkill();
return;
}
try {
// Save to backend
await API.put(`/api/characters/${this.character.id}`, this.character);
const response = await API.put(`/api/characters/${this.character.id}`, this.character);
console.log('Skill updated successfully:', skill.name, field, newValue);
// Update the parent component with the response data
// The backend now returns the full FeChar with categorizedskills
this.$emit('character-data-updated', response.data);
this.$emit('character-updated');
this.cancelEditSkill();
} catch (error) {
console.error('Failed to update skill:', error);
alert('Fehler beim Speichern: ' + (error.response?.data?.error || error.message));
this.cancelEditSkill();
// Revert changes on error by reloading
this.$emit('character-updated');
}
},
@@ -799,6 +868,7 @@ export default {
this.editingSkillId = null;
this.editingField = null;
this.editValue = '';
this.showNameEditWarning = false;
},
isEditingSkill(skill, field) {
@@ -129,6 +129,7 @@
{{ $t('spell.category') }}
<button @click="sortBy('category')">{{ sortField === 'category' ? (sortAsc ? '' : '') : '-' }}</button>
</th>
<th class="cd-table-header">{{ $t('spell.learning_category') || 'Learning Category' }}</th>
<th class="cd-table-header">
{{ $t('spell.name') }}
<button @click="sortBy('name')">{{ sortField === 'name' ? (sortAsc ? '' : '') : '-' }}</button>
@@ -150,7 +151,7 @@
<tbody>
<tr v-if="creatingNew">
<td>New</td>
<td colspan="14">
<td colspan="15">
<div class="edit-form">
<div class="edit-row">
<div class="edit-field">
@@ -165,6 +166,15 @@
</option>
</select>
</div>
<div class="edit-field">
<label>{{ $t('spell.learning_category') || 'Learning Category' }}:</label>
<select v-model="newItem.learning_category" style="width:150px;">
<option value="">-</option>
<option v-for="category in availableLearnCategories" :key="category" :value="category">
{{ category }}
</option>
</select>
</div>
<div class="edit-field">
<label>{{ $t('spell.level') }}:</label>
<input v-model.number="newItem.level" type="number" style="width:60px;" />
@@ -248,6 +258,7 @@
<tr v-if="editingIndex !== index">
<td>{{ dtaItem.id || '' }}</td>
<td>{{ dtaItem.category|| '-' }}</td>
<td>{{ dtaItem.learning_category || '-' }}</td>
<td>{{ dtaItem.name || '-' }}</td>
<td>{{ dtaItem.level || '0' }}</td>
<td>{{ dtaItem.ap || '0' }}</td>
@@ -267,7 +278,7 @@
<!-- Edit Mode -->
<tr v-else>
<td><input v-model="editedItem.id" style="width:20px;" disabled /></td>
<td colspan="14">
<td colspan="15">
<!-- Expanded edit form -->
<div class="edit-form">
<div class="edit-row">
@@ -283,6 +294,15 @@
</option>
</select>
</div>
<div class="edit-field">
<label>{{ $t('spell.learning_category') || 'Learning Category' }}:</label>
<select v-model="editedItem.learning_category" style="width:150px;">
<option value="">-</option>
<option v-for="category in availableLearnCategories" :key="category" :value="category">
{{ category }}
</option>
</select>
</div>
<div class="edit-field">
<label>{{ $t('spell.level') }}:</label>
<input v-model.number="editedItem.level" type="number" style="width:60px;" />
@@ -519,6 +539,7 @@ export default {
filterQuelle: '',
enhancedSpells: [],
availableSources: [],
availableLearnCategories: [],
gameSystems: [],
selectedSystemId: null,
creatingNew: false,
@@ -663,6 +684,7 @@ export default {
const response = await API.get('/api/maintenance/spells-enhanced')
this.enhancedSpells = response.data.spells || []
this.availableSources = response.data.sources || []
this.availableLearnCategories = response.data.learnCategories || []
// Also update mdata for compatibility
if (response.data.categories) {
this.mdata.spellcategories = response.data.categories
@@ -725,6 +747,7 @@ export default {
this.newItem = {
name: '',
category: this.mdata.spellcategories?.[0] || '',
learning_category: '',
level: 0,
ap: '',
zauberdauer: '',
+3 -1
View File
@@ -177,6 +177,7 @@ export default {
spell:{
id:'ID',
category:'Kategorie',
learning_category:'Lernkategorie',
name:'Name',
description:'Beschreibung',
level:'Stufe',
@@ -563,7 +564,8 @@ export default {
bRollTooltip: 'B würfeln: 1d6 + Modifikator je nach Rasse'
},
datasheet: {
editHelp: 'Doppelklicken auf ein Feld, um es zu bearbeiten.'
editHelp: 'Doppelklicken auf ein Feld, um es zu bearbeiten.',
editnamewarning: 'Wenn der Name der Fertigkeit geändert wird kann diese nicht mehr nach den Regeln verbessert werden. Auch beim Export können Boni und andere Werte fehlen!'
}
},
audit: {
+3 -1
View File
@@ -173,6 +173,7 @@ export default {
spell:{
id:'ID',
category:'Category',
learning_category:'Learning Category',
name:'Name',
description:'Description',
level:'Level',
@@ -559,7 +560,8 @@ export default {
bRollTooltip: 'Roll B: 1d6 + modifier based on race'
},
datasheet: {
editHelp: 'Click twice on a field to edit it.'
editHelp: 'Click twice on a field to edit it.',
editnamewarning: 'If the name of the skill is changed, it can no longer be improved according to the rules. Also, bonuses and other values may be missing when exporting!'
}
},
audit: {