added lern and skill improvement cost editing

This commit is contained in:
2026-04-16 22:16:11 +02:00
parent c9a8b5771a
commit c684123ffc
10 changed files with 1783 additions and 221 deletions
+3 -1
View File
@@ -39,6 +39,7 @@ import GameSystemView from "./maintenance/GameSystemView.vue";
import LitSourceView from "./maintenance/LitSourceView.vue";
import MiscLookupView from "./maintenance/MiscLookupView.vue";
import SkillImprovementCostView from "./maintenance/SkillImprovementCostView.vue";
import LearningCostView from "./maintenance/LearningCostView.vue";
export default {
@@ -55,6 +56,7 @@ export default {
LitSourceView,
MiscLookupView,
SkillImprovementCostView,
LearningCostView,
},
data() {
return {
@@ -82,7 +84,7 @@ export default {
{ id: 7, name: "litsource", component: "LitSourceView" },
{ id: 8, name: "misc", component: "MiscLookupView" },
{ id: 9, name: "skillimprovement", component: "SkillImprovementCostView" },
{ id: 10, name: "learningcost", component: "LearningCostView" },
],
};
},
@@ -0,0 +1,440 @@
<template>
<div class="header-section">
<h2>{{ $t('maintenance') }} - {{ $t('learningcost.title') }}</h2>
</div>
<div v-if="error" class="error-box">{{ error }}</div>
<div v-if="isLoading" class="cd-view">
<p>{{ $t('common.loading') }}</p>
</div>
<!-- Section 1: EP per TE Class × Skill Category matrix -->
<div v-if="!isLoading" class="cd-view">
<h3>{{ $t('learningcost.headerEPPerTE') }}</h3>
<p class="lc-description">{{ $t('learningcost.epPerTEDesc') }}</p>
<div class="cd-list">
<table class="cd-table lc-matrix">
<thead>
<tr>
<th class="cd-table-header lc-sticky-col">{{ $t('learningcost.class') }}</th>
<th v-for="cat in skillCategories" :key="cat.id" class="cd-table-header">
{{ cat.name }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="cc in characterClasses" :key="cc.code">
<td class="lc-sticky-col lc-row-header">{{ cc.code }}</td>
<td
v-for="cat in skillCategories"
:key="cat.id"
class="lc-cell"
@click="startEditEPPerTE(cc.code, cat.name)"
>
<template v-if="editingCell === cellKey('te', cc.code, cat.name)">
<input
v-model.number="editValue"
type="number"
min="0"
class="lc-input"
@keyup.enter="saveEditEPPerTE(cc.code, cat.name)"
@keyup.escape="cancelEdit"
@blur="saveEditEPPerTE(cc.code, cat.name)"
ref="cellInput"
/>
</template>
<template v-else>
{{ getEPPerTE(cc.code, cat.name) ?? '-' }}
</template>
</td>
</tr>
</tbody>
</table>
</div>
<p class="lc-note">{{ $t('learningcost.goldCostTE') }}</p>
<p class="lc-hint">{{ $t('learningcost.clickToEdit') }}</p>
</div>
<!-- Section 2: EP per LE Class × Spell School matrix -->
<div v-if="!isLoading" class="cd-view">
<h3>{{ $t('learningcost.headerEPPerLE') }}</h3>
<p class="lc-description">{{ $t('learningcost.epPerLEDesc') }}</p>
<div class="cd-list">
<table class="cd-table lc-matrix">
<thead>
<tr>
<th class="cd-table-header lc-sticky-col">{{ $t('learningcost.class') }}</th>
<th v-for="school in spellSchools" :key="school.id" class="cd-table-header">
{{ school.name }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="cc in spellCasterClasses" :key="cc.code">
<td class="lc-sticky-col lc-row-header">{{ cc.code }}</td>
<td
v-for="school in spellSchools"
:key="school.id"
class="lc-cell"
@click="startEditEPPerLE(cc.code, school.name)"
>
<template v-if="editingCell === cellKey('le', cc.code, school.name)">
<input
v-model.number="editValue"
type="number"
min="0"
class="lc-input"
@keyup.enter="saveEditEPPerLE(cc.code, school.name)"
@keyup.escape="cancelEdit"
@blur="saveEditEPPerLE(cc.code, school.name)"
ref="cellInput"
/>
</template>
<template v-else>
{{ getEPPerLE(cc.code, school.name) ?? '-' }}
</template>
</td>
</tr>
</tbody>
</table>
</div>
<p class="lc-note">{{ $t('learningcost.goldCostLE') }}</p>
<p class="lc-note">{{ $t('learningcost.maSpecialNote') }}</p>
<p class="lc-note">{{ $t('learningcost.scrollLearnNote') }}</p>
<p class="lc-hint">{{ $t('learningcost.clickToEdit') }}</p>
</div>
<!-- Section 3: LE per Spell Level -->
<div v-if="!isLoading" class="cd-view">
<h3>{{ $t('learningcost.headerSpellLevelLE') }}</h3>
<p class="lc-description">{{ $t('learningcost.spellLevelLEDesc') }}</p>
<div class="cd-list">
<table class="cd-table">
<thead>
<tr>
<th class="cd-table-header">{{ $t('learningcost.spellLevel') }}</th>
<th class="cd-table-header">{{ $t('learningcost.leRequired') }}</th>
<th class="cd-table-header"></th>
</tr>
</thead>
<tbody>
<template v-for="cost in spellLevelCosts" :key="cost.id">
<tr v-if="editingSpellLevelId !== cost.id">
<td>{{ cost.level }}</td>
<td>{{ cost.le_required }}</td>
<td><button @click="startEditSpellLevel(cost)">{{ $t('common.edit') }}</button></td>
</tr>
<tr v-else>
<td>{{ cost.level }}</td>
<td>
<input
v-model.number="editSpellLevelValue"
type="number"
min="0"
/>
</td>
<td>
<div class="edit-actions">
<button class="btn-primary btn-save" :disabled="isSaving" @click="saveEditSpellLevel">
<span v-if="!isSaving">{{ $t('common.save') }}</span>
<span v-else>{{ $t('common.saving') }}</span>
</button>
<button class="btn-cancel" :disabled="isSaving" @click="cancelEditSpellLevel">
{{ $t('common.cancel') }}
</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<style scoped>
.lc-description {
margin: 8px 0;
font-style: italic;
opacity: 0.8;
}
.lc-note {
margin: 4px 0;
font-size: 0.9em;
font-style: italic;
opacity: 0.75;
}
.lc-hint {
margin-top: 6px;
font-size: 0.85em;
opacity: 0.6;
}
.lc-matrix {
min-width: max-content;
}
.lc-matrix th,
.lc-matrix td {
text-align: center;
min-width: 60px;
white-space: nowrap;
}
.lc-sticky-col {
position: sticky;
left: 0;
background: var(--color-background);
z-index: 1;
}
.lc-row-header {
text-align: left;
font-weight: bold;
padding-right: 12px;
}
.lc-cell {
cursor: pointer;
transition: background 0.15s ease;
}
.lc-cell:hover {
background: var(--color-background-mute);
}
.lc-input {
width: 60px;
text-align: center;
padding: 2px 4px;
border: 1px solid var(--color-border);
border-radius: 3px;
}
.error-box {
margin: 10px 0;
padding: 10px 12px;
background: #ffe3e3;
color: #8a1c1c;
border: 1px solid #f5c2c2;
border-radius: 6px;
}
.edit-actions {
display: flex;
gap: 10px;
}
</style>
<script>
import API from '../../utils/api'
export default {
name: 'LearningCostView',
data() {
return {
characterClasses: [],
skillCategories: [],
spellSchools: [],
epPerTECosts: [],
epPerLECosts: [],
spellLevelCosts: [],
isLoading: false,
isSaving: false,
error: '',
// Matrix cell editing
editingCell: null,
editValue: 0,
// Spell level editing
editingSpellLevelId: null,
editSpellLevelValue: 0,
}
},
computed: {
spellCasterClasses() {
const codesWithSpells = new Set(this.epPerLECosts.map(c => c.characterClass || c.character_class?.code))
return this.characterClasses.filter(cc => codesWithSpells.has(cc.code))
},
},
async created() {
await this.loadAll()
},
methods: {
cellKey(type, classCode, colName) {
return `${type}:${classCode}:${colName}`
},
// --- Data loading ---
async loadAll() {
this.isLoading = true
this.error = ''
try {
const [classesResp, categoriesResp, schoolsResp, epTEResp, epLEResp, spellLevelResp] = await Promise.all([
API.get('/api/maintenance/character-classes'),
API.get('/api/maintenance/skill-categories'),
API.get('/api/maintenance/spell-schools'),
API.get('/api/maintenance/class-category-ep-costs'),
API.get('/api/maintenance/class-spell-school-ep-costs'),
API.get('/api/maintenance/spell-level-le-costs'),
])
this.characterClasses = classesResp.data?.character_classes || []
this.skillCategories = categoriesResp.data?.skill_categories || []
this.spellSchools = schoolsResp.data?.spell_schools || []
this.epPerTECosts = epTEResp.data?.costs || []
this.epPerLECosts = epLEResp.data?.costs || []
this.spellLevelCosts = spellLevelResp.data?.costs || []
} catch (err) {
console.error('Failed to load learning costs:', err)
this.error = err.response?.data?.error || err.message
} finally {
this.isLoading = false
}
},
// --- EP per TE matrix ---
getEPPerTE(classCode, categoryName) {
const entry = this.epPerTECosts.find(
c => (c.characterClass === classCode || c.character_class?.code === classCode) &&
(c.skillCategory === categoryName || c.skill_category?.name === categoryName)
)
return entry?.ep_per_te
},
findEPPerTEEntry(classCode, categoryName) {
return this.epPerTECosts.find(
c => (c.characterClass === classCode || c.character_class?.code === classCode) &&
(c.skillCategory === categoryName || c.skill_category?.name === categoryName)
)
},
startEditEPPerTE(classCode, categoryName) {
const entry = this.findEPPerTEEntry(classCode, categoryName)
if (!entry) return
this.cancelEdit()
this.editingCell = this.cellKey('te', classCode, categoryName)
this.editValue = entry.ep_per_te
this.$nextTick(() => {
const inputs = this.$refs.cellInput
if (inputs) {
const el = Array.isArray(inputs) ? inputs[0] : inputs
el?.focus()
el?.select()
}
})
},
async saveEditEPPerTE(classCode, categoryName) {
const entry = this.findEPPerTEEntry(classCode, categoryName)
if (!entry || this.editingCell !== this.cellKey('te', classCode, categoryName)) return
if (entry.ep_per_te === this.editValue) {
this.cancelEdit()
return
}
this.isSaving = true
try {
const resp = await API.put(`/api/maintenance/class-category-ep-costs/${entry.id}`, {
ep_per_te: this.editValue,
})
const idx = this.epPerTECosts.findIndex(c => c.id === entry.id)
if (idx !== -1) this.epPerTECosts.splice(idx, 1, resp.data)
this.cancelEdit()
} catch (err) {
console.error('Failed to save EP/TE cost:', err)
this.error = err.response?.data?.error || err.message
} finally {
this.isSaving = false
}
},
// --- EP per LE matrix ---
getEPPerLE(classCode, schoolName) {
const entry = this.epPerLECosts.find(
c => (c.characterClass === classCode || c.character_class?.code === classCode) &&
(c.spellSchool === schoolName || c.spell_school?.name === schoolName)
)
return entry?.ep_per_le
},
findEPPerLEEntry(classCode, schoolName) {
return this.epPerLECosts.find(
c => (c.characterClass === classCode || c.character_class?.code === classCode) &&
(c.spellSchool === schoolName || c.spell_school?.name === schoolName)
)
},
startEditEPPerLE(classCode, schoolName) {
const entry = this.findEPPerLEEntry(classCode, schoolName)
if (!entry) return
this.cancelEdit()
this.editingCell = this.cellKey('le', classCode, schoolName)
this.editValue = entry.ep_per_le
this.$nextTick(() => {
const inputs = this.$refs.cellInput
if (inputs) {
const el = Array.isArray(inputs) ? inputs[0] : inputs
el?.focus()
el?.select()
}
})
},
async saveEditEPPerLE(classCode, schoolName) {
const entry = this.findEPPerLEEntry(classCode, schoolName)
if (!entry || this.editingCell !== this.cellKey('le', classCode, schoolName)) return
if (entry.ep_per_le === this.editValue) {
this.cancelEdit()
return
}
this.isSaving = true
try {
const resp = await API.put(`/api/maintenance/class-spell-school-ep-costs/${entry.id}`, {
ep_per_le: this.editValue,
})
const idx = this.epPerLECosts.findIndex(c => c.id === entry.id)
if (idx !== -1) this.epPerLECosts.splice(idx, 1, resp.data)
this.cancelEdit()
} catch (err) {
console.error('Failed to save EP/LE cost:', err)
this.error = err.response?.data?.error || err.message
} finally {
this.isSaving = false
}
},
// --- Spell Level LE costs ---
startEditSpellLevel(cost) {
this.cancelEdit()
this.editingSpellLevelId = cost.id
this.editSpellLevelValue = cost.le_required
},
cancelEditSpellLevel() {
this.editingSpellLevelId = null
this.editSpellLevelValue = 0
},
async saveEditSpellLevel() {
if (this.editingSpellLevelId == null) return
this.isSaving = true
try {
const resp = await API.put(`/api/maintenance/spell-level-le-costs/${this.editingSpellLevelId}`, {
le_required: this.editSpellLevelValue,
})
const idx = this.spellLevelCosts.findIndex(c => c.id === this.editingSpellLevelId)
if (idx !== -1) this.spellLevelCosts.splice(idx, 1, resp.data)
this.cancelEditSpellLevel()
} catch (err) {
console.error('Failed to save spell level LE cost:', err)
this.error = err.response?.data?.error || err.message
} finally {
this.isSaving = false
}
},
// --- Shared ---
cancelEdit() {
this.editingCell = null
this.editValue = 0
this.editingSpellLevelId = null
this.editSpellLevelValue = 0
},
},
}
</script>
@@ -1,117 +1,200 @@
<template>
<div class="header-section">
<h2>{{ $t('maintenance') }} - {{ $t('skillimprovement.title') }}</h2>
<button class="btn-primary" @click="startCreate">{{ $t('newEntry') }}</button>
</div>
<div v-if="error" class="error-box">{{ error }}</div>
<div class="cd-view">
<div class="cd-list">
<table class="cd-table">
<thead>
<tr>
<th class="cd-table-header">{{ $t('skillimprovement.id') }}</th>
<th class="cd-table-header">{{ $t('skillimprovement.level') }}</th>
<th class="cd-table-header">{{ $t('skillimprovement.te') }}</th>
<th class="cd-table-header">{{ $t('skillimprovement.category') }}</th>
<th class="cd-table-header">{{ $t('skillimprovement.difficulty') }}</th>
<th class="cd-table-header"></th>
</tr>
</thead>
<tbody>
<tr v-if="isLoading">
<td colspan="6">{{ $t('common.loading') }}</td>
</tr>
<tr v-if="creatingNew">
<td>New</td>
<td colspan="5">
<div class="edit-form">
<div class="edit-row">
<label>{{ $t('skillimprovement.level') }}</label>
<input v-model.number="newItem.current_level" type="number" />
<label class="inline-label">{{ $t('skillimprovement.te') }}</label>
<input v-model.number="newItem.te_required" type="number" />
</div>
<div class="edit-row">
<label>{{ $t('skillimprovement.category') }}</label>
<select v-model.number="newItem.category_id">
<option v-for="cat in categoryOptions" :key="cat.id" :value="cat.id">
{{ cat.label }}
</option>
</select>
<label class="inline-label">{{ $t('skillimprovement.difficulty') }}</label>
<select v-model.number="newItem.difficulty_id">
<option v-for="diff in difficultyOptions" :key="diff.id" :value="diff.id">
{{ diff.label }}
</option>
</select>
</div>
<div class="edit-actions">
<button class="btn-primary btn-save" :disabled="isSaving" @click="saveCreate">
<span v-if="!isSaving">{{ $t('common.save') }}</span>
<span v-else>{{ $t('common.saving') }}</span>
</button>
<button class="btn-cancel" :disabled="isSaving" @click="cancelCreate">
{{ $t('common.cancel') }}
</button>
</div>
</div>
</td>
</tr>
<template v-for="cost in costs" :key="cost.id">
<tr v-if="editingId !== cost.id">
<td>{{ cost.id }}</td>
<td>{{ cost.current_level }}</td>
<td>{{ cost.te_required }}</td>
<td>{{ displayCategory(cost) }}</td>
<td>{{ displayDifficulty(cost) }}</td>
<td><button @click="startEdit(cost)">{{ $t('common.edit') }}</button></td>
<p class="si-description">{{ $t('skillimprovement.description') }}</p>
<div class="si-category-tabs">
<button
v-for="cat in availableCategories"
:key="cat.id"
:class="{ active: selectedCategoryId === cat.id }"
@click="selectedCategoryId = cat.id"
>
{{ cat.name }}
</button>
</div>
<div v-if="isLoading" class="cd-view">
<p>{{ $t('common.loading') }}</p>
</div>
<template v-if="!isLoading && selectedCategoryId">
<!-- Lernen section -->
<div class="cd-view">
<h3>{{ $t('skillimprovement.lernen') }}</h3>
<div class="cd-list">
<table class="cd-table">
<thead>
<tr>
<th class="cd-table-header">{{ $t('skillimprovement.difficulty') }}</th>
<th class="cd-table-header">{{ $t('skillimprovement.le') }}</th>
<th class="cd-table-header">{{ $t('skillimprovement.skills') }}</th>
</tr>
<tr v-else>
<td>{{ cost.id }}</td>
<td colspan="5">
<div class="edit-form">
<div class="edit-row">
<label>{{ $t('skillimprovement.level') }}</label>
<input v-model.number="editedItem.current_level" type="number" />
<label class="inline-label">{{ $t('skillimprovement.te') }}</label>
<input v-model.number="editedItem.te_required" type="number" />
</div>
<div class="edit-row">
<label>{{ $t('skillimprovement.category') }}</label>
<select v-model.number="editedItem.category_id">
<option v-for="cat in categoryOptions" :key="cat.id" :value="cat.id">
{{ cat.label }}
</option>
</select>
<label class="inline-label">{{ $t('skillimprovement.difficulty') }}</label>
<select v-model.number="editedItem.difficulty_id">
<option v-for="diff in difficultyOptions" :key="diff.id" :value="diff.id">
{{ diff.label }}
</option>
</select>
</div>
<div class="edit-actions">
<button class="btn-primary btn-save" :disabled="isSaving" @click="saveEdit">
<span v-if="!isSaving">{{ $t('common.save') }}</span>
<span v-else>{{ $t('common.saving') }}</span>
</button>
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
{{ $t('common.cancel') }}
</button>
</div>
</div>
</thead>
<tbody>
<tr v-for="row in lernenRows" :key="row.key">
<td>{{ row.difficultyName }}</td>
<td
class="si-cell"
@click="startEditLernen(row)"
>
<template v-if="editingLernenKey === row.key">
<input
v-model.number="editLernenValue"
type="number"
min="0"
class="si-input"
@keyup.enter="saveLernenEdit(row)"
@keyup.escape="cancelLernenEdit"
@blur="saveLernenEdit(row)"
ref="lernenInput"
/>
<span class="si-le-unit">LE</span>
</template>
<template v-else>
{{ row.learnCost }} LE
</template>
</td>
<td>{{ row.skillsDisplay }}</td>
</tr>
<tr v-if="lernenRows.length === 0">
<td colspan="3" class="si-empty">{{ $t('skillimprovement.noLernenData') }}</td>
</tr>
</tbody>
</table>
</div>
<p class="si-hint">{{ $t('skillimprovement.clickLeToEdit') }}</p>
</div>
<!-- Verbessern (TE) section -->
<div class="cd-view">
<h3>{{ $t('skillimprovement.verbessern') }}</h3>
<div class="cd-list">
<table class="cd-table si-matrix">
<thead>
<tr>
<th class="cd-table-header si-sticky-col">{{ $t('skillimprovement.difficulty') }}</th>
<th v-for="level in matrixLevels" :key="level" class="cd-table-header">
+{{ level }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="diff in matrixDifficulties" :key="diff.id">
<td class="si-sticky-col si-row-header">{{ diff.name }}</td>
<td
v-for="level in matrixLevels"
:key="level"
class="si-cell"
@click="startEditTE(diff.id, level)"
>
<template v-if="editingCell === cellKey(diff.id, level)">
<input
v-model.number="editValue"
type="number"
min="0"
class="si-input"
@keyup.enter="saveEditTE(diff.id, level)"
@keyup.escape="cancelEdit"
@blur="saveEditTE(diff.id, level)"
ref="cellInput"
/>
</template>
<template v-else>
{{ displayTE(diff.id, level) }}
</template>
</td>
</tr>
</template>
</tbody>
</table>
<tr v-if="matrixDifficulties.length === 0">
<td :colspan="matrixLevels.length + 1" class="si-empty">{{ $t('skillimprovement.noVerbessernData') }}</td>
</tr>
</tbody>
</table>
</div>
<p class="si-hint">{{ $t('skillimprovement.clickToEdit') }}</p>
</div>
</div>
</template>
</template>
<style scoped>
.si-description {
margin: 8px 0 4px;
font-style: italic;
opacity: 0.8;
}
.si-category-tabs {
display: flex;
gap: 4px;
margin: 10px 0;
flex-wrap: wrap;
}
.si-category-tabs button {
padding: 6px 14px;
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--color-background-soft);
cursor: pointer;
transition: all 0.2s ease;
}
.si-category-tabs button:hover {
background: var(--color-background-mute);
}
.si-category-tabs button.active {
background: var(--color-background-mute);
font-weight: bold;
border-color: var(--color-text);
}
.si-matrix {
min-width: max-content;
}
.si-matrix th,
.si-matrix td {
text-align: center;
min-width: 50px;
white-space: nowrap;
}
.si-sticky-col {
position: sticky;
left: 0;
background: var(--color-background);
z-index: 1;
}
.si-row-header {
text-align: left;
font-weight: bold;
padding-right: 12px;
}
.si-cell {
cursor: pointer;
transition: background 0.15s ease;
}
.si-cell:hover {
background: var(--color-background-mute);
}
.si-input {
width: 55px;
text-align: center;
padding: 2px 4px;
border: 1px solid var(--color-border);
border-radius: 3px;
}
.si-hint {
margin-top: 6px;
font-size: 0.85em;
opacity: 0.6;
}
.si-le-unit {
margin-left: 2px;
font-size: 0.9em;
}
.si-empty {
font-style: italic;
opacity: 0.6;
}
.error-box {
margin: 10px 0;
padding: 10px 12px;
@@ -120,24 +203,6 @@
border: 1px solid #f5c2c2;
border-radius: 6px;
}
.edit-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.edit-row {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.edit-actions {
display: flex;
gap: 10px;
}
.inline-label {
margin-left: 10px;
}
</style>
<script>
@@ -147,127 +212,226 @@ export default {
name: 'SkillImprovementCostView',
data() {
return {
costs: [],
editingId: null,
editedItem: null,
creatingNew: false,
newItem: null,
categories: [],
difficulties: [],
skillCatDiffs: [],
improvementCosts: [],
selectedCategoryId: null,
isLoading: false,
isSaving: false,
error: '',
editingCell: null,
editValue: 0,
editingLernenKey: null,
editLernenValue: 0,
}
},
async created() {
await this.loadCosts()
},
computed: {
categoryOptions() {
const seen = new Map()
this.costs.forEach(c => {
const id = c.category_id ?? c.skillCategoryId
const name = c.category_name || c.skillCategoryName
if (id != null && !seen.has(id)) {
seen.set(id, name ? `${name} (${id})` : `${id}`)
}
})
return Array.from(seen.entries()).map(([id, label]) => ({ id, label }))
availableCategories() {
const catIdsWithSKD = new Set(this.skillCatDiffs.map(d => d.skill_category?.id))
const catIdsWithIC = new Set(this.improvementCosts.map(c => c.skillCategoryId))
return this.categories.filter(cat =>
catIdsWithSKD.has(cat.id) || catIdsWithIC.has(cat.id)
)
},
difficultyOptions() {
const seen = new Map()
this.costs.forEach(c => {
const id = c.difficulty_id ?? c.skillDifficultyId
const name = c.difficulty_name || c.skillDifficultyName
if (id != null && !seen.has(id)) {
seen.set(id, name ? `${name} (${id})` : `${id}`)
lernenRows() {
if (!this.selectedCategoryId) return []
const items = this.skillCatDiffs.filter(d =>
d.skill_category?.id === this.selectedCategoryId
)
const groups = new Map()
for (const item of items) {
const diffName = item.skill_difficulty?.name || item.skillDifficulty || ''
const diffId = item.skill_difficulty?.id || 0
const lc = item.learn_cost
const key = `${diffId}:${lc}`
if (!groups.has(key)) {
groups.set(key, {
key,
difficultyId: diffId,
difficultyName: diffName,
learnCost: lc,
itemIds: [],
skills: [],
})
}
})
return Array.from(seen.entries()).map(([id, label]) => ({ id, label }))
groups.get(key).itemIds.push(item.id)
const skill = item.skill
if (skill) {
const be = skill.bonuseigenschaft || '-'
groups.get(key).skills.push(`${skill.name}+${skill.initialwert} (${be})`)
}
}
const rows = Array.from(groups.values())
rows.sort((a, b) => a.difficultyId - b.difficultyId || a.learnCost - b.learnCost)
for (const row of rows) {
row.skills.sort()
row.skillsDisplay = row.skills.join(', ')
}
return rows
},
categoryImprovementCosts() {
if (!this.selectedCategoryId) return []
return this.improvementCosts.filter(c => c.skillCategoryId === this.selectedCategoryId)
},
matrixLevels() {
const levels = [...new Set(this.categoryImprovementCosts.map(c => c.current_level))]
levels.sort((a, b) => a - b)
return levels
},
matrixDifficulties() {
const seen = new Map()
for (const c of this.categoryImprovementCosts) {
const id = c.skillDifficultyId
if (!seen.has(id)) {
const name = c.difficulty_name || this.difficulties.find(d => d.id === id)?.name || `${id}`
seen.set(id, { id, name })
}
}
return Array.from(seen.values()).sort((a, b) => a.id - b.id)
},
},
async created() {
await this.loadAll()
},
methods: {
displayCategory(cost) {
return cost.category_name || cost.skillCategoryName || cost.skillCategoryId || cost.category_id
cellKey(diffId, level) {
return `${diffId}:${level}`
},
displayDifficulty(cost) {
return cost.difficulty_name || cost.skillDifficultyName || cost.skillDifficultyId || cost.difficulty_id
},
async loadCosts() {
async loadAll() {
this.isLoading = true
this.error = ''
try {
const resp = await API.get('/api/maintenance/skill-improvement-cost2')
this.costs = resp.data?.costs || []
const [catResp, diffResp, scdResp, icResp] = await Promise.all([
API.get('/api/maintenance/skill-categories'),
API.get('/api/maintenance/skill-difficulties'),
API.get('/api/maintenance/skill-category-difficulties'),
API.get('/api/maintenance/skill-improvement-cost2'),
])
this.categories = catResp.data?.skill_categories || []
this.difficulties = diffResp.data?.skill_difficulties || []
this.skillCatDiffs = scdResp.data?.items || []
this.improvementCosts = icResp.data?.costs || []
if (this.availableCategories.length > 0 && !this.selectedCategoryId) {
this.selectedCategoryId = this.availableCategories[0].id
}
} catch (err) {
console.error('Failed to load costs:', err)
console.error('Failed to load skill improvement data:', err)
this.error = err.response?.data?.error || err.message
} finally {
this.isLoading = false
}
},
startEdit(cost) {
this.editingId = cost.id
this.editedItem = {
...cost,
category_id: cost.category_id ?? cost.skillCategoryId,
difficulty_id: cost.difficulty_id ?? cost.skillDifficultyId,
}
findImprovementCost(diffId, level) {
return this.categoryImprovementCosts.find(
c => c.skillDifficultyId === diffId && c.current_level === level
)
},
cancelEdit() {
this.editingId = null
this.editedItem = null
displayTE(diffId, level) {
const entry = this.findImprovementCost(diffId, level)
if (!entry) return '-'
return entry.te_required === 0 ? '-' : entry.te_required
},
startCreate() {
startEditTE(diffId, level) {
const entry = this.findImprovementCost(diffId, level)
if (!entry) return
this.cancelEdit()
const defaultCategory = this.categoryOptions[0]?.id ?? null
const defaultDifficulty = this.difficultyOptions[0]?.id ?? null
this.newItem = {
current_level: 0,
te_required: 0,
category_id: defaultCategory,
difficulty_id: defaultDifficulty,
}
this.creatingNew = true
this.editingCell = this.cellKey(diffId, level)
this.editValue = entry.te_required
this.$nextTick(() => {
const inputs = this.$refs.cellInput
if (inputs) {
const el = Array.isArray(inputs) ? inputs[0] : inputs
el?.focus()
el?.select()
}
})
},
cancelCreate() {
this.creatingNew = false
this.newItem = null
},
async saveEdit() {
if (!this.editedItem) return
const payload = {
current_level: this.editedItem.current_level,
te_required: this.editedItem.te_required,
category_id: this.editedItem.category_id,
difficulty_id: this.editedItem.difficulty_id,
async saveEditTE(diffId, level) {
const entry = this.findImprovementCost(diffId, level)
if (!entry || this.editingCell !== this.cellKey(diffId, level)) return
if (entry.te_required === this.editValue) {
this.cancelEdit()
return
}
this.isSaving = true
try {
const resp = await API.put(`/api/maintenance/skill-improvement-cost2/${this.editingId}`, payload)
const idx = this.costs.findIndex(c => c.id === this.editingId)
if (idx !== -1) this.costs.splice(idx, 1, resp.data)
const resp = await API.put(`/api/maintenance/skill-improvement-cost2/${entry.id}`, {
current_level: entry.current_level,
te_required: this.editValue,
category_id: entry.skillCategoryId,
difficulty_id: entry.skillDifficultyId,
})
const idx = this.improvementCosts.findIndex(c => c.id === entry.id)
if (idx !== -1) this.improvementCosts.splice(idx, 1, resp.data)
this.cancelEdit()
} catch (err) {
console.error('Failed to save cost:', err)
console.error('Failed to save TE cost:', err)
this.error = err.response?.data?.error || err.message
} finally {
this.isSaving = false
}
},
async saveCreate() {
if (!this.newItem) return
const payload = {
current_level: this.newItem.current_level,
te_required: this.newItem.te_required,
category_id: this.newItem.category_id,
difficulty_id: this.newItem.difficulty_id,
cancelEdit() {
this.editingCell = null
this.editValue = 0
},
startEditLernen(row) {
this.cancelEdit()
this.editingLernenKey = row.key
this.editLernenValue = row.learnCost
this.$nextTick(() => {
const inputs = this.$refs.lernenInput
if (inputs) {
const el = Array.isArray(inputs) ? inputs[0] : inputs
el?.focus()
el?.select()
}
})
},
cancelLernenEdit() {
this.editingLernenKey = null
this.editLernenValue = 0
},
async saveLernenEdit(row) {
if (this.editingLernenKey !== row.key) return
if (row.learnCost === this.editLernenValue) {
this.cancelLernenEdit()
return
}
const newValue = this.editLernenValue
this.cancelLernenEdit()
this.isSaving = true
try {
const resp = await API.post('/api/maintenance/skill-improvement-cost2', payload)
this.costs.push(resp.data)
this.cancelCreate()
for (const id of row.itemIds) {
const resp = await API.put(`/api/maintenance/skill-category-difficulties/${id}`, {
learn_cost: newValue,
})
const idx = this.skillCatDiffs.findIndex(d => d.id === id)
if (idx !== -1) this.skillCatDiffs.splice(idx, 1, resp.data)
}
} catch (err) {
console.error('Failed to create cost:', err)
console.error('Failed to save learn cost:', err)
this.error = err.response?.data?.error || err.message
} finally {
this.isSaving = false
+29 -10
View File
@@ -306,6 +306,7 @@ export default {
litsource:'Literaturquellen',
misc:'Sonstige',
skillimprovement:'Steigerungskosten',
learningcost:'Lernkosten',
},
believe: {
title: 'Glaubensrichtungen',
@@ -364,17 +365,35 @@ export default {
saving: 'Speichern...',
cancel: 'Abbrechen'
},
learningcost: {
title: 'Lernkosten',
headerEPPerTE: 'EP-Kosten für 1 Trainingseinheit (TE)',
headerEPPerLE: 'EP-Kosten für 1 Lerneinheit (LE) für Zauber',
headerSpellLevelLE: 'Benötigte LE pro Zauberstufe',
class: 'Klasse',
epPerTEDesc: '1 Lerneinheit (LE) kostet das Dreifache an EP (+ 6 EP für Elfen).',
epPerLEDesc: 'EP-Kosten für 1 Lerneinheit (LE) für Zauber nach Charakterklasse und Zauberschule (+ 6 EP für Elfen).',
spellLevelLEDesc: 'Benötigte Lerneinheiten (LE) pro Zauberstufe.',
goldCostTE: 'Geldkosten: 20 GS je TE, 200 GS je LE.',
goldCostLE: 'Geldkosten: 100 GS je LE.',
maSpecialNote: '* Ma erhalten LE für Sprüche aus ihrem Spezialgebiet für 30 EP.',
scrollLearnNote: 'Lernen von Spruchrollen: 1/3 EP je LE bei Erfolg, pauschal 20 GS je Lernversuch.',
spellLevel: 'Zauberstufe',
leRequired: 'LE erforderlich',
clickToEdit: 'Klicke auf einen Wert um ihn zu bearbeiten.',
},
skillimprovement: {
title: 'Fertigkeitssteigerungskosten',
id: 'ID',
level: 'Aktueller Wert',
te: 'TE erforderlich',
category: 'Kategorie-ID',
difficulty: 'Schwierigkeits-ID',
edit: 'Bearbeiten',
save: 'Speichern',
saving: 'Speichern...',
cancel: 'Abbrechen'
title: 'Lern- und Trainingslisten',
description: 'Die Zahl an Lern- und Trainingseinheiten f\u00fcr ein und dieselbe Fertigkeit stimmen in allen Gruppen \u00fcberein. Die Gruppenzugeh\u00f6rigkeit entscheidet nur dar\u00fcber, wie viele EP der Abenteurer abh\u00e4ngig von seinem Typ f\u00fcr LE und TE bezahlen muss.',
lernen: 'Lernen',
verbessern: 'Verbessern (TE)',
difficulty: 'Schwierigkeit',
le: 'LE',
skills: 'Fertigkeiten',
noLernenData: 'Keine Lerndaten f\u00fcr diese Kategorie vorhanden.',
noVerbessernData: 'Keine Verbesserungsdaten f\u00fcr diese Kategorie vorhanden.',
clickToEdit: 'Klicke auf einen Wert um ihn zu bearbeiten.',
clickLeToEdit: 'Klicke auf einen LE-Wert um ihn zu bearbeiten.',
},
search:'Suche',
newEntry:'Neuer Eintrag',
+29 -10
View File
@@ -302,6 +302,7 @@ export default {
litsource:'Sources',
misc:'Misc',
skillimprovement:'Improvement Costs',
learningcost:'Learning Costs',
},
believe: {
title: 'Beliefs',
@@ -360,17 +361,35 @@ export default {
saving: 'Saving...',
cancel: 'Cancel'
},
learningcost: {
title: 'Learning Costs',
headerEPPerTE: 'EP Costs per Training Unit (TE)',
headerEPPerLE: 'EP Costs per Learning Unit (LE) for Spells',
headerSpellLevelLE: 'Required LE per Spell Level',
class: 'Class',
epPerTEDesc: '1 Learning Unit (LE) costs triple the EP (+ 6 EP for Elves).',
epPerLEDesc: 'EP costs for 1 Learning Unit (LE) for spells by character class and spell school (+ 6 EP for Elves).',
spellLevelLEDesc: 'Required learning units (LE) per spell level.',
goldCostTE: 'Gold costs: 20 GS per TE, 200 GS per LE.',
goldCostLE: 'Gold costs: 100 GS per LE.',
maSpecialNote: '* Magicians receive LE for spells in their specialization for 30 EP.',
scrollLearnNote: 'Learning from spell scrolls: 1/3 EP per LE on success, flat 20 GS per attempt.',
spellLevel: 'Spell Level',
leRequired: 'LE required',
clickToEdit: 'Click a value to edit it.',
},
skillimprovement: {
title: 'Skill Improvement Costs',
id: 'ID',
level: 'Current level',
te: 'TE required',
category: 'Category ID',
difficulty: 'Difficulty ID',
edit: 'Edit',
save: 'Save',
saving: 'Saving...',
cancel: 'Cancel'
title: 'Learning & Training Lists',
description: 'The number of learning and training units for any given skill is the same across all groups. Group membership only determines how many EP the adventurer must pay for LE and TE based on their type.',
lernen: 'Learning',
verbessern: 'Improving (TE)',
difficulty: 'Difficulty',
le: 'LE',
skills: 'Skills',
noLernenData: 'No learning data for this category.',
noVerbessernData: 'No improvement data for this category.',
clickToEdit: 'Click a value to edit it.',
clickLeToEdit: 'Click an LE value to edit it.',
},
search:'Search',
newEntry:'New Entry',