editing and maintenance and user suggestions

* every user has a right of a username and a display name and has the right to change it
* System information page now shows information about database user count, char count, and database schema version
* more maintenance lists
* show the right values in columns and fields
* move similar from inside of frontend component functions to utility js when used multiple times
* display help on mouse over
* add more than one believe to character
* make char name editable with better char info in headline
* GiT Gifttoleranz value not calculated correctly
* Bump backend to 0.2.3, frontend to 0.2.2
This commit is contained in:
Bardioc26
2026-02-03 17:21:43 +01:00
committed by GitHub
parent 49e6a684dd
commit 95f0fc0b7a
51 changed files with 3513 additions and 118 deletions
@@ -1,4 +1,4 @@
package config
package appsystem
import (
"github.com/gin-gonic/gin"
@@ -8,3 +8,7 @@ import (
func Versionsinfo(c *gin.Context) {
c.JSON(200, GetInfo())
}
func SystemInfo(c *gin.Context) {
c.JSON(200, GetInfo2())
}
@@ -1,10 +1,11 @@
package config
package appsystem
import "github.com/gin-gonic/gin"
// RegisterRoutes registers config-related routes (protected)
func RegisterRoutes(r *gin.RouterGroup) {
r.GET("/version", Versionsinfo)
r.GET("/systeminfo", SystemInfo)
}
// RegisterPublicRoutes registers public config routes (no auth required)
@@ -12,4 +13,5 @@ func RegisterPublicRoutes(r *gin.Engine) {
// Public version endpoint - no authentication required
public := r.Group("/api/public")
public.GET("/version", Versionsinfo)
public.GET("/systeminfo", SystemInfo)
}
@@ -1,4 +1,8 @@
package config
package appsystem
import (
"bamort/database"
)
// Version is the application version
const Version = "0.2.2"
@@ -50,6 +54,22 @@ type Info struct {
Version string `json:"version"`
GitCommit string `json:"gitCommit"`
}
type Info2 struct {
Version string `json:"version"`
GitCommit string `json:"gitCommit"`
UserCount int64 `json:"userCount"`
CharCount int64 `json:"charCount"`
DbVersion string `json:"dbVersion"`
}
func getUserCount() int {
// Placeholder implementation
return 42
}
func getCharCount() int {
// Placeholder implementation
return 123456
}
// GetInfo returns version information as a struct
func GetInfo() Info {
@@ -58,3 +78,20 @@ func GetInfo() Info {
GitCommit: GitCommit,
}
}
func GetInfo2() Info2 {
info := Info2{
Version: Version,
GitCommit: GitCommit,
}
db := database.DB
if db == nil {
return info
}
db.Table("char_chars").Count(&info.CharCount)
db.Table("users").Count(&info.UserCount)
db.Raw("SELECT version FROM schema_version ORDER BY id DESC LIMIT 1").Scan(&info.DbVersion)
return info
}
@@ -1,7 +1,10 @@
package config
package appsystem
import (
"testing"
"bamort/database"
"bamort/testutils"
)
func TestGetVersion(t *testing.T) {
@@ -41,3 +44,24 @@ func TestGetInfo(t *testing.T) {
t.Errorf("Expected info.Version %s, got %s", Version, info.Version)
}
}
func TestGetInfo2UsesDatabaseValues(t *testing.T) {
testutils.SetupTestEnvironment(t)
database.ResetTestDB()
t.Cleanup(database.ResetTestDB)
database.SetupTestDB()
info := GetInfo2()
if info.CharCount <= 0 {
t.Fatalf("expected character count from database, got %d", info.CharCount)
}
if info.UserCount <= 0 {
t.Fatalf("expected user count from database, got %d", info.UserCount)
}
if info.DbVersion == "" {
t.Fatalf("expected database schema version, got empty string")
}
}
+1
View File
@@ -195,6 +195,7 @@ func ToFeChar(object *models.Char) *models.FeChar {
feC.Fertigkeiten = skills
feC.InnateSkills = innateSkills
feC.CategorizedSkills = categories
feC.Git = object.GetGiftToleranz()
return feC
}
+3 -1
View File
@@ -1073,9 +1073,11 @@ func TestGetDatasheetOptions(t *testing.T) {
assert.Contains(t, socialClasses, "Mittelschicht")
faiths := response["faiths"].([]interface{})
assert.Equal(t, 5, len(faiths))
assert.Equal(t, 15, len(faiths))
assert.Contains(t, faiths, "Druide")
assert.Contains(t, faiths, "Keine")
assert.Contains(t, faiths, "Torkin")
assert.NotContains(t, faiths, "")
handedness := response["handedness"].([]interface{})
assert.Equal(t, 3, len(handedness))
+15 -11
View File
@@ -33,8 +33,9 @@ func GetCharacterShares(c *gin.Context) {
// Get user details for each share
type ShareWithUser struct {
models.CharShare
Username string `json:"username"`
Email string `json:"email"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
Email string `json:"email"`
}
var sharesWithUsers []ShareWithUser
@@ -42,9 +43,10 @@ func GetCharacterShares(c *gin.Context) {
var u user.User
if err := u.FirstId(share.UserID); err == nil {
sharesWithUsers = append(sharesWithUsers, ShareWithUser{
CharShare: share,
Username: u.Username,
Email: u.Email,
CharShare: share,
Username: u.Username,
DisplayName: u.DisplayNameOrUsername(),
Email: u.Email,
})
}
}
@@ -128,17 +130,19 @@ func GetAvailableUsersForSharing(c *gin.Context) {
// Remove sensitive data
type UserInfo struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
Email string `json:"email"`
UserID uint `json:"user_id"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
Email string `json:"email"`
}
var userInfos []UserInfo
for _, u := range users {
userInfos = append(userInfos, UserInfo{
UserID: u.UserID,
Username: u.Username,
Email: u.Email,
UserID: u.UserID,
Username: u.Username,
DisplayName: u.DisplayNameOrUsername(),
Email: u.Email,
})
}
+93
View File
@@ -0,0 +1,93 @@
package character
import (
"bamort/database"
"bamort/models"
"bamort/user"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupShareHandlerTestEnvironment(t *testing.T) {
original := os.Getenv("ENVIRONMENT")
os.Setenv("ENVIRONMENT", "test")
t.Cleanup(func() {
if original != "" {
os.Setenv("ENVIRONMENT", original)
} else {
os.Unsetenv("ENVIRONMENT")
}
})
database.SetupTestDB(true, true)
t.Cleanup(database.ResetTestDB)
err := user.MigrateStructure()
require.NoError(t, err, "Should migrate user structure")
err = models.MigrateStructure()
require.NoError(t, err, "Should migrate models structure")
gin.SetMode(gin.TestMode)
}
func createHashedUser(t *testing.T, username, password, email, displayName string) *user.User {
u := &user.User{
Username: username,
PasswordHash: password,
Email: email,
DisplayName: displayName,
}
hashed := md5.Sum([]byte(password))
u.PasswordHash = hex.EncodeToString(hashed[:])
err := u.Create()
require.NoError(t, err, "Should create user")
return u
}
func TestGetAvailableUsersForSharingReturnsDisplayNames(t *testing.T) {
setupShareHandlerTestEnvironment(t)
owner := createHashedUser(t, "owneruser", "ownerpass", "owner@example.com", "")
sharedUser := createHashedUser(t, "shareduser", "sharedpass", "shared@example.com", "Shared Display")
char := models.Char{
BamortBase: models.BamortBase{Name: "Shared Character"},
UserID: owner.UserID,
}
err := database.DB.Create(&char).Error
require.NoError(t, err, "Should create character")
req, _ := http.NewRequest("GET", fmt.Sprintf("/api/characters/%d/available-users", char.ID), nil)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
c.Params = gin.Params{gin.Param{Key: "id", Value: fmt.Sprintf("%d", char.ID)}}
c.Set("userID", owner.UserID)
GetAvailableUsersForSharing(c)
assert.Equal(t, http.StatusOK, w.Code)
var response []map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
require.Len(t, response, 1, "Only the non-owner user should be returned")
entry := response[0]
assert.Equal(t, float64(sharedUser.UserID), entry["user_id"])
assert.Equal(t, sharedUser.DisplayName, entry["display_name"])
}
+3 -2
View File
@@ -1,6 +1,7 @@
package main
import (
"bamort/appsystem"
"bamort/character"
"bamort/config"
"bamort/database"
@@ -92,11 +93,11 @@ func main() {
importer.RegisterRoutes(protected)
pdfrender.RegisterRoutes(protected)
transfer.RegisterRoutes(protected)
config.RegisterRoutes(protected)
appsystem.RegisterRoutes(protected)
// Register public routes (no authentication)
pdfrender.RegisterPublicRoutes(r)
config.RegisterPublicRoutes(r)
appsystem.RegisterPublicRoutes(r)
logger.Info("API-Routen erfolgreich registriert")
+97
View File
@@ -4,6 +4,7 @@ import (
"bamort/database"
"bamort/models"
"fmt"
"sort"
"strings"
)
@@ -19,23 +20,32 @@ func GetMiscLookupByKey(key string, order ...string) ([]models.MiscLookup, error
func GetMiscLookupByKeyForSystem(key string, gameSystemId uint, order ...string) ([]models.MiscLookup, error) {
var items []models.MiscLookup
orderKey := "value"
orderBy := "value ASC"
if len(order) > 0 && order[0] != "" {
switch order[0] {
case "id":
orderKey = "id"
orderBy = "id ASC"
case "value":
orderKey = "value"
orderBy = "value ASC"
case "source":
orderKey = "source"
orderBy = "source_id ASC, value ASC"
case "source_value":
orderKey = "source"
orderBy = "source_id ASC, value ASC"
default:
orderKey = "value"
orderBy = "value ASC"
}
}
gs := models.GetGameSystem(gameSystemId, "")
if gs == nil {
gs = models.GetGameSystem(0, "")
}
query := database.DB.Where("`key` = ?", key)
if gs.ID != 0 {
@@ -57,9 +67,96 @@ func GetMiscLookupByKeyForSystem(key string, gameSystemId uint, order ...string)
}
}
if key == "faiths" {
existingValues := make(map[string]struct{}, len(items))
for i := range items {
trimmed := strings.TrimSpace(items[i].Value)
if trimmed == "" {
continue
}
existingValues[trimmed] = struct{}{}
}
believeItems, err := collectBeliefsForSystem(gs)
if err != nil {
return items, err
}
additions := make([]models.MiscLookup, 0, len(believeItems))
for _, believe := range believeItems {
value := strings.TrimSpace(believe.Name)
if value == "" {
continue
}
if _, exists := existingValues[value]; exists {
continue
}
existingValues[value] = struct{}{}
addition := models.MiscLookup{
ID: believe.ID,
Key: key,
Value: value,
SourceID: believe.SourceID,
PageNumber: believe.PageNumber,
GameSystem: believe.GameSystem,
GameSystemId: believe.GameSystemId,
}
if addition.GameSystemId == 0 && gs.ID != 0 {
addition.GameSystemId = gs.ID
}
additions = append(additions, addition)
}
if len(additions) > 0 {
items = append(items, additions...)
sortMiscLookup(items, orderKey)
}
}
return items, nil
}
func collectBeliefsForSystem(gs *models.GameSystem) ([]models.Believe, error) {
var believes []models.Believe
if gs == nil {
return believes, nil
}
query := database.DB.Model(&models.Believe{})
if gs.ID != 0 {
query = query.Where("game_system_id = ?", gs.ID)
}
if gs.Name != "" {
query = query.Or("game_system = ?", gs.Name)
}
if err := query.Find(&believes).Error; err != nil {
return believes, err
}
return believes, nil
}
func sortMiscLookup(items []models.MiscLookup, orderKey string) {
sort.SliceStable(items, func(i, j int) bool {
switch orderKey {
case "id":
return items[i].ID < items[j].ID
case "source":
if items[i].SourceID == items[j].SourceID {
return items[i].Value < items[j].Value
}
return items[i].SourceID < items[j].SourceID
default:
return items[i].Value < items[j].Value
}
})
}
/*
// PopulateMiscLookupData populates initial misc lookup data if table is empty
func PopulateMiscLookupData() error {
+16 -1
View File
@@ -111,7 +111,7 @@ func TestPopulateMiscLookupData(t *testing.T) {
"races": 5,
"origins": 15,
"social_classes": 4,
"faiths": 5,
"faiths": 15,
"handedness": 3,
}
@@ -133,6 +133,21 @@ func TestPopulateMiscLookupData(t *testing.T) {
assert.Contains(t, raceValues, "Mensch")
assert.Contains(t, raceValues, "Elf")
faiths, _ := GetMiscLookupByKey("faiths")
faithValues := make([]string, len(faiths))
for i, f := range faiths {
faithValues[i] = f.Value
}
assert.Contains(t, faithValues, "Torkin")
assert.NotContains(t, faithValues, "")
mahalCount := 0
for _, v := range faithValues {
if v == "Mahal" {
mahalCount++
}
}
assert.Equal(t, 1, mahalCount)
/*
// Second population should not duplicate data
err = PopulateMiscLookupData()
+155
View File
@@ -0,0 +1,155 @@
package maintenance
import (
"bamort/database"
"bamort/models"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
type believeResponse struct {
models.Believe
SourceCode string `json:"source_code,omitempty"`
}
type believeUpdateRequest struct {
Name string `json:"name"`
Beschreibung string `json:"beschreibung"`
SourceID *uint `json:"source_id"`
PageNumber *int `json:"page_number"`
}
func resolveGameSystemOrDefault(c *gin.Context) *models.GameSystem {
gsIDStr := c.Query("game_system_id")
gsName := c.Query("game_system")
var gsID uint
if gsIDStr != "" {
id, err := strconv.ParseUint(gsIDStr, 10, 32)
if err != nil {
respondWithError(c, http.StatusBadRequest, "Invalid game_system_id")
return nil
}
gsID = uint(id)
}
gs := models.GetGameSystem(gsID, gsName)
if gs == nil {
respondWithError(c, http.StatusBadRequest, "Invalid game system")
return nil
}
return gs
}
func GetBelieves(c *gin.Context) {
gs := resolveGameSystemOrDefault(c)
if gs == nil {
return
}
var believes []models.Believe
if err := database.DB.Where("game_system=? OR game_system_id=?", gs.Name, gs.ID).
Order("name ASC").
Find(&believes).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve believes")
return
}
var sources []models.Source
if err := database.DB.Find(&sources).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve sources")
return
}
sourceMap := make(map[uint]string, len(sources))
for _, source := range sources {
sourceMap[source.ID] = source.Code
}
enhanced := make([]believeResponse, len(believes))
for i, believe := range believes {
enhanced[i] = believeResponse{
Believe: believe,
SourceCode: sourceMap[believe.SourceID],
}
}
c.JSON(http.StatusOK, gin.H{
"believes": enhanced,
"sources": sources,
})
}
func UpdateBelieve(c *gin.Context) {
gs := resolveGameSystemOrDefault(c)
if gs == nil {
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
respondWithError(c, http.StatusBadRequest, "Invalid ID")
return
}
var believe models.Believe
if err := database.DB.First(&believe, uint(id)).Error; err != nil {
respondWithError(c, http.StatusNotFound, "Believe not found")
return
}
var payload believeUpdateRequest
if err := c.ShouldBindJSON(&payload); err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
name := strings.TrimSpace(payload.Name)
if name == "" {
respondWithError(c, http.StatusBadRequest, "name is required")
return
}
believe.Name = name
believe.Beschreibung = payload.Beschreibung
if payload.SourceID != nil {
believe.SourceID = *payload.SourceID
} else {
believe.SourceID = 0
}
if payload.PageNumber != nil {
believe.PageNumber = *payload.PageNumber
} else {
believe.PageNumber = 0
}
believe.GameSystem = gs.Name
believe.GameSystemId = gs.ID
if err := database.DB.Save(&believe).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to update believe")
return
}
c.JSON(http.StatusOK, believeResponse{
Believe: believe,
SourceCode: lookupSourceCode(believe.SourceID),
})
}
func lookupSourceCode(sourceID uint) string {
if sourceID == 0 {
return ""
}
var source models.Source
if err := database.DB.Select("code").First(&source, sourceID).Error; err != nil {
return ""
}
return source.Code
}
@@ -0,0 +1,160 @@
package maintenance
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"bamort/database"
"bamort/models"
"bamort/router"
"bamort/testutils"
"bamort/user"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupMaintenanceTest(t *testing.T) (string, *gin.Engine, *models.GameSystem) {
t.Helper()
testutils.SetupTestEnvironment(t)
database.ResetTestDB()
t.Cleanup(database.ResetTestDB)
database.SetupTestDB(true)
var maintainer user.User
require.NoError(t, database.DB.First(&maintainer, 1).Error)
maintainer.Role = user.RoleMaintainer
require.NoError(t, maintainer.Save())
token := user.GenerateToken(&maintainer)
gin.SetMode(gin.TestMode)
r := gin.Default()
router.SetupGin(r)
protected := router.BaseRouterGrp(r)
RegisterRoutes(protected)
gs := models.GetGameSystem(0, "")
require.NotNil(t, gs)
return token, r, gs
}
func createSource(t *testing.T, gs *models.GameSystem, code string) models.Source {
t.Helper()
source := models.Source{
Code: code,
Name: fmt.Sprintf("Source %s", code),
FullName: fmt.Sprintf("Source %s", code),
GameSystem: gs.Name,
GameSystemId: gs.ID,
IsActive: true,
}
require.NoError(t, database.DB.Create(&source).Error)
return source
}
func createBelieve(t *testing.T, gs *models.GameSystem, source models.Source, name string) models.Believe {
t.Helper()
believe := models.Believe{
Name: name,
Beschreibung: "Initial description",
SourceID: source.ID,
PageNumber: 7,
GameSystem: gs.Name,
GameSystemId: gs.ID,
}
require.NoError(t, database.DB.Create(&believe).Error)
return believe
}
func TestListBelievesReturnsData(t *testing.T) {
token, router, gs := setupMaintenanceTest(t)
source := createSource(t, gs, "TSTBEL")
created := createBelieve(t, gs, source, "Test Believe One")
req, err := http.NewRequest(http.MethodGet, "/api/maintenance/gsm-believes", nil)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+token)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
require.Equal(t, http.StatusOK, resp.Code)
var payload struct {
Believes []struct {
ID uint `json:"id"`
Name string `json:"name"`
SourceID uint `json:"source_id"`
SourceCode string `json:"source_code"`
} `json:"believes"`
Sources []models.Source `json:"sources"`
}
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &payload))
assert.NotEmpty(t, payload.Sources)
var found bool
for _, b := range payload.Believes {
if b.ID == created.ID {
found = true
assert.Equal(t, created.Name, b.Name)
assert.Equal(t, source.ID, b.SourceID)
assert.Equal(t, source.Code, b.SourceCode)
}
}
assert.True(t, found, "expected created believe in response")
}
func TestUpdateBelieve(t *testing.T) {
token, router, gs := setupMaintenanceTest(t)
sourceOld := createSource(t, gs, "OLD01")
sourceNew := createSource(t, gs, "NEW01")
created := createBelieve(t, gs, sourceOld, "Old Believe")
updateBody := map[string]interface{}{
"name": "Updated Believe",
"beschreibung": "Updated description",
"source_id": sourceNew.ID,
"page_number": 123,
}
bodyBytes, err := json.Marshal(updateBody)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/api/maintenance/gsm-believes/%d", created.ID), bytes.NewBuffer(bodyBytes))
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
require.Equal(t, http.StatusOK, resp.Code)
var updated models.Believe
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &updated))
assert.Equal(t, created.ID, updated.ID)
assert.Equal(t, "Updated Believe", updated.Name)
assert.Equal(t, "Updated description", updated.Beschreibung)
assert.Equal(t, sourceNew.ID, updated.SourceID)
assert.Equal(t, 123, updated.PageNumber)
assert.Equal(t, gs.Name, updated.GameSystem)
assert.Equal(t, gs.ID, updated.GameSystemId)
var stored models.Believe
require.NoError(t, database.DB.First(&stored, created.ID).Error)
assert.Equal(t, "Updated Believe", stored.Name)
assert.Equal(t, sourceNew.ID, stored.SourceID)
assert.Equal(t, 123, stored.PageNumber)
}
+356
View File
@@ -0,0 +1,356 @@
package maintenance
import (
"bamort/database"
"bamort/models"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
func GetGameSystems(c *gin.Context) {
var systems []models.GameSystem
if err := database.DB.Order("code ASC").Find(&systems).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve game systems")
return
}
c.JSON(http.StatusOK, gin.H{"game_systems": systems})
}
type gameSystemUpdateRequest struct {
Name string `json:"name"`
Description string `json:"description"`
IsActive *bool `json:"is_active"`
}
func UpdateGameSystem(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
respondWithError(c, http.StatusBadRequest, "Invalid ID")
return
}
var gs models.GameSystem
if err := database.DB.First(&gs, uint(id)).Error; err != nil {
respondWithError(c, http.StatusNotFound, "Game system not found")
return
}
var payload gameSystemUpdateRequest
if err := c.ShouldBindJSON(&payload); err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
name := strings.TrimSpace(payload.Name)
if name != "" {
gs.Name = name
}
gs.Description = payload.Description
if payload.IsActive != nil {
gs.IsActive = *payload.IsActive
}
if err := database.DB.Save(&gs).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to update game system")
return
}
c.JSON(http.StatusOK, gs)
}
// --- Sources ---
type sourceUpdateRequest struct {
Name string `json:"name"`
FullName string `json:"full_name"`
Edition string `json:"edition"`
Publisher string `json:"publisher"`
PublishYear int `json:"publish_year"`
Description string `json:"description"`
IsCore *bool `json:"is_core"`
IsActive *bool `json:"is_active"`
}
func GetLitSources(c *gin.Context) {
gs := resolveGameSystemOrDefault(c)
if gs == nil {
return
}
var sources []models.Source
if err := database.DB.Where("game_system=? OR game_system_id=?", gs.Name, gs.ID).
Order("code ASC").
Find(&sources).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve sources")
return
}
c.JSON(http.StatusOK, gin.H{"sources": sources})
}
func UpdateLitSource(c *gin.Context) {
gs := resolveGameSystemOrDefault(c)
if gs == nil {
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
respondWithError(c, http.StatusBadRequest, "Invalid ID")
return
}
var src models.Source
if err := database.DB.First(&src, uint(id)).Error; err != nil {
respondWithError(c, http.StatusNotFound, "Source not found")
return
}
var payload sourceUpdateRequest
if err := c.ShouldBindJSON(&payload); err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
if name := strings.TrimSpace(payload.Name); name != "" {
src.Name = name
}
src.FullName = payload.FullName
src.Edition = payload.Edition
src.Publisher = payload.Publisher
src.PublishYear = payload.PublishYear
src.Description = payload.Description
if payload.IsCore != nil {
src.IsCore = *payload.IsCore
}
if payload.IsActive != nil {
src.IsActive = *payload.IsActive
}
src.GameSystem = gs.Name
src.GameSystemId = gs.ID
if err := database.DB.Save(&src).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to update source")
return
}
c.JSON(http.StatusOK, src)
}
// --- Misc lookup ---
type miscUpdateRequest struct {
Key string `json:"key"`
Value string `json:"value"`
SourceID *uint `json:"source_id"`
PageNumber *int `json:"page_number"`
}
func GetMisc(c *gin.Context) {
gs := resolveGameSystemOrDefault(c)
if gs == nil {
return
}
keyFilter := strings.TrimSpace(c.Query("key"))
var items []models.MiscLookup
q := database.DB.Where("game_system=? OR game_system_id=?", gs.Name, gs.ID)
if keyFilter != "" {
q = q.Where("`key` = ?", keyFilter)
}
if err := q.Order("`key` ASC, value ASC").Find(&items).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve misc")
return
}
c.JSON(http.StatusOK, gin.H{"misc": items})
}
func UpdateMisc(c *gin.Context) {
gs := resolveGameSystemOrDefault(c)
if gs == nil {
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
respondWithError(c, http.StatusBadRequest, "Invalid ID")
return
}
var item models.MiscLookup
if err := database.DB.First(&item, uint(id)).Error; err != nil {
respondWithError(c, http.StatusNotFound, "Misc entry not found")
return
}
var payload miscUpdateRequest
if err := c.ShouldBindJSON(&payload); err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
if key := strings.TrimSpace(payload.Key); key != "" {
item.Key = key
}
if payload.Value != "" {
item.Value = payload.Value
}
if payload.SourceID != nil {
item.SourceID = *payload.SourceID
}
if payload.PageNumber != nil {
item.PageNumber = *payload.PageNumber
}
item.GameSystem = gs.Name
item.GameSystemId = gs.ID
if err := database.DB.Save(&item).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to update misc entry")
return
}
c.JSON(http.StatusOK, item)
}
// --- Skill improvement cost2 ---
type skillImprovementUpdateRequest struct {
CurrentLevel *int `json:"current_level"`
TERequired *int `json:"te_required"`
CategoryID *uint `json:"category_id"`
DifficultyID *uint `json:"difficulty_id"`
}
func GetSkillImprovementCost2(c *gin.Context) {
var costs []models.SkillImprovementCost
if err := database.DB.Order("current_level ASC").Find(&costs).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve skill improvement costs")
return
}
categoryNames, difficultyNames, err := loadSkillMetadata(costs)
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve skill improvement costs")
return
}
responses := make([]skillImprovementCostResponse, len(costs))
for i, cost := range costs {
responses[i] = skillImprovementCostResponse{
SkillImprovementCost: cost,
CategoryName: categoryNames[cost.CategoryID],
DifficultyName: difficultyNames[cost.DifficultyID],
}
}
c.JSON(http.StatusOK, gin.H{"costs": responses})
}
func UpdateSkillImprovementCost2(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
respondWithError(c, http.StatusBadRequest, "Invalid ID")
return
}
var cost models.SkillImprovementCost
if err := database.DB.First(&cost, uint(id)).Error; err != nil {
respondWithError(c, http.StatusNotFound, "Skill improvement cost not found")
return
}
var payload skillImprovementUpdateRequest
if err := c.ShouldBindJSON(&payload); err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
if payload.CurrentLevel != nil {
cost.CurrentLevel = *payload.CurrentLevel
}
if payload.TERequired != nil {
cost.TERequired = *payload.TERequired
}
if payload.CategoryID != nil {
cost.CategoryID = *payload.CategoryID
}
if payload.DifficultyID != nil {
cost.DifficultyID = *payload.DifficultyID
}
if err := database.DB.Save(&cost).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to update skill improvement cost")
return
}
categoryNames, difficultyNames, err := loadSkillMetadata([]models.SkillImprovementCost{cost})
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to update skill improvement cost")
return
}
c.JSON(http.StatusOK, skillImprovementCostResponse{
SkillImprovementCost: cost,
CategoryName: categoryNames[cost.CategoryID],
DifficultyName: difficultyNames[cost.DifficultyID],
})
}
type skillImprovementCostResponse struct {
models.SkillImprovementCost
CategoryName string `json:"category_name,omitempty"`
DifficultyName string `json:"difficulty_name,omitempty"`
}
func loadSkillMetadata(costs []models.SkillImprovementCost) (map[uint]string, map[uint]string, error) {
categoryNames := make(map[uint]string)
difficultyNames := make(map[uint]string)
if len(costs) == 0 {
return categoryNames, difficultyNames, nil
}
categoryIDs := make([]uint, 0)
difficultyIDs := make([]uint, 0)
seenCategories := make(map[uint]struct{})
seenDifficulties := make(map[uint]struct{})
for _, cost := range costs {
if _, ok := seenCategories[cost.CategoryID]; !ok {
seenCategories[cost.CategoryID] = struct{}{}
categoryIDs = append(categoryIDs, cost.CategoryID)
}
if _, ok := seenDifficulties[cost.DifficultyID]; !ok {
seenDifficulties[cost.DifficultyID] = struct{}{}
difficultyIDs = append(difficultyIDs, cost.DifficultyID)
}
}
if len(categoryIDs) > 0 {
var categories []models.SkillCategory
if err := database.DB.Where("id IN ?", categoryIDs).Find(&categories).Error; err != nil {
return nil, nil, err
}
for _, category := range categories {
categoryNames[category.ID] = category.Name
}
}
if len(difficultyIDs) > 0 {
var difficulties []models.SkillDifficulty
if err := database.DB.Where("id IN ?", difficultyIDs).Find(&difficulties).Error; err != nil {
return nil, nil, err
}
for _, difficulty := range difficulties {
difficultyNames[difficulty.ID] = difficulty.Name
}
}
return categoryNames, difficultyNames, nil
}
@@ -0,0 +1,292 @@
package maintenance
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"bamort/database"
"bamort/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func createGameSystem(t *testing.T, code string) models.GameSystem {
t.Helper()
gs := models.GameSystem{Code: code, Name: "Game System " + code, Description: "desc", IsActive: true}
require.NoError(t, database.DB.Create(&gs).Error)
return gs
}
func TestListGameSystems(t *testing.T) {
token, router, _ := setupMaintenanceTest(t)
created := createGameSystem(t, "TSTGS")
req, err := http.NewRequest(http.MethodGet, "/api/maintenance/game-systems", nil)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+token)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
require.Equal(t, http.StatusOK, resp.Code)
var payload struct {
GameSystems []models.GameSystem `json:"game_systems"`
}
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &payload))
var found bool
for _, gs := range payload.GameSystems {
if gs.ID == created.ID {
found = true
assert.Equal(t, created.Code, gs.Code)
assert.Equal(t, created.Name, gs.Name)
}
}
assert.True(t, found, "expected created game system in response")
}
func TestUpdateGameSystem(t *testing.T) {
token, router, _ := setupMaintenanceTest(t)
gs := createGameSystem(t, "UPDGS")
body := map[string]interface{}{
"name": "Updated GS",
"description": "Updated desc",
"is_active": false,
}
bodyBytes, _ := json.Marshal(body)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/api/maintenance/game-systems/%d", gs.ID), bytes.NewBuffer(bodyBytes))
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
require.Equal(t, http.StatusOK, resp.Code)
var updated models.GameSystem
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &updated))
assert.Equal(t, gs.ID, updated.ID)
assert.Equal(t, "Updated GS", updated.Name)
assert.Equal(t, "Updated desc", updated.Description)
assert.False(t, updated.IsActive)
}
func createLitSource(t *testing.T, gs models.GameSystem, code string) models.Source {
t.Helper()
src := models.Source{
Code: code,
Name: "Source " + code,
FullName: "Full " + code,
Edition: "1",
Publisher: "Pub",
PublishYear: 2025,
Description: "Desc",
IsCore: false,
IsActive: true,
GameSystem: gs.Name,
GameSystemId: gs.ID,
}
require.NoError(t, database.DB.Create(&src).Error)
return src
}
func TestListLitSources(t *testing.T) {
token, router, gs := setupMaintenanceTest(t)
src := createLitSource(t, *gs, "SRC01")
req, _ := http.NewRequest(http.MethodGet, "/api/maintenance/gsm-lit-sources", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
require.Equal(t, http.StatusOK, resp.Code)
var payload struct {
Sources []models.Source `json:"sources"`
}
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &payload))
var found bool
for _, s := range payload.Sources {
if s.ID == src.ID {
found = true
assert.Equal(t, src.Code, s.Code)
assert.Equal(t, src.Name, s.Name)
}
}
assert.True(t, found)
}
func TestUpdateLitSource(t *testing.T) {
token, router, gs := setupMaintenanceTest(t)
src := createLitSource(t, *gs, "SRCUPD")
body := map[string]interface{}{
"name": "Updated Source",
"full_name": "Updated Full",
"edition": "2",
"publisher": "NewPub",
"publish_year": 2026,
"description": "New Desc",
"is_active": false,
"is_core": true,
}
payload, _ := json.Marshal(body)
req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("/api/maintenance/gsm-lit-sources/%d", src.ID), bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
require.Equal(t, http.StatusOK, resp.Code)
var updated models.Source
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &updated))
assert.Equal(t, "Updated Source", updated.Name)
assert.Equal(t, "Updated Full", updated.FullName)
assert.Equal(t, "2", updated.Edition)
assert.Equal(t, "NewPub", updated.Publisher)
assert.Equal(t, 2026, updated.PublishYear)
assert.False(t, updated.IsActive)
assert.True(t, updated.IsCore)
}
func createMisc(t *testing.T, gs models.GameSystem, key, value string) models.MiscLookup {
t.Helper()
m := models.MiscLookup{
Key: key,
Value: value,
GameSystem: gs.Name,
GameSystemId: gs.ID,
}
require.NoError(t, database.DB.Create(&m).Error)
return m
}
func TestListMisc(t *testing.T) {
token, router, gs := setupMaintenanceTest(t)
item := createMisc(t, *gs, "origin", "North")
req, _ := http.NewRequest(http.MethodGet, "/api/maintenance/gsm-misc", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
require.Equal(t, http.StatusOK, resp.Code)
var payload struct {
Items []models.MiscLookup `json:"misc"`
}
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &payload))
var found bool
for _, it := range payload.Items {
if it.ID == item.ID {
found = true
assert.Equal(t, "origin", it.Key)
assert.Equal(t, "North", it.Value)
}
}
assert.True(t, found)
}
func TestUpdateMisc(t *testing.T) {
token, router, gs := setupMaintenanceTest(t)
item := createMisc(t, *gs, "race", "Human")
body := map[string]interface{}{
"value": "Elf",
}
payload, _ := json.Marshal(body)
req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("/api/maintenance/gsm-misc/%d", item.ID), bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
require.Equal(t, http.StatusOK, resp.Code)
var updated models.MiscLookup
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &updated))
assert.Equal(t, "Elf", updated.Value)
}
func createSkillImprovementCost(t *testing.T, gs models.GameSystem) models.SkillImprovementCost {
t.Helper()
cost := models.SkillImprovementCost{
CurrentLevel: 5,
TERequired: 2,
CategoryID: 1,
DifficultyID: 1,
}
require.NoError(t, database.DB.Create(&cost).Error)
return cost
}
func TestListSkillImprovementCost(t *testing.T) {
token, router, gs := setupMaintenanceTest(t)
created := createSkillImprovementCost(t, *gs)
req, _ := http.NewRequest(http.MethodGet, "/api/maintenance/skill-improvement-cost2", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
require.Equal(t, http.StatusOK, resp.Code)
var payload struct {
Costs []models.SkillImprovementCost `json:"costs"`
}
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &payload))
var found bool
for _, c := range payload.Costs {
if c.ID == created.ID {
found = true
assert.Equal(t, created.CurrentLevel, c.CurrentLevel)
assert.Equal(t, created.TERequired, c.TERequired)
}
}
assert.True(t, found)
}
func TestUpdateSkillImprovementCost(t *testing.T) {
token, router, gs := setupMaintenanceTest(t)
created := createSkillImprovementCost(t, *gs)
body := map[string]interface{}{
"te_required": 5,
"current_level": 6,
}
payload, _ := json.Marshal(body)
req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("/api/maintenance/skill-improvement-cost2/%d", created.ID), bytes.NewBuffer(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
require.Equal(t, http.StatusOK, resp.Code)
var updated models.SkillImprovementCost
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &updated))
assert.Equal(t, 5, updated.TERequired)
assert.Equal(t, 6, updated.CurrentLevel)
}
+10
View File
@@ -10,6 +10,16 @@ func RegisterRoutes(r *gin.RouterGroup) {
charGrp := r.Group("/maintenance")
charGrp.Use(user.RequireMaintainer())
{
charGrp.GET("/gsm-believes", GetBelieves)
charGrp.PUT("/gsm-believes/:id", UpdateBelieve)
charGrp.GET("/game-systems", GetGameSystems)
charGrp.PUT("/game-systems/:id", UpdateGameSystem)
charGrp.GET("/gsm-lit-sources", GetLitSources)
charGrp.PUT("/gsm-lit-sources/:id", UpdateLitSource)
charGrp.GET("/gsm-misc", GetMisc)
charGrp.PUT("/gsm-misc/:id", UpdateMisc)
charGrp.GET("/skill-improvement-cost2", GetSkillImprovementCost2)
charGrp.PUT("/skill-improvement-cost2/:id", UpdateSkillImprovementCost2)
charGrp.GET("/setupcheck", SetupCheck)
charGrp.GET("/setupcheck-dev", SetupCheckDev)
charGrp.GET("/mktestdata", MakeTestdataFromLive)
+9 -3
View File
@@ -131,6 +131,7 @@ type CharList struct {
type FeChar struct {
Char
Git int `json:"git"` // GiftToleranz
CategorizedSkills map[string][]SkFertigkeit `json:"categorizedskills"`
InnateSkills []SkFertigkeit `json:"innateskills"`
}
@@ -140,6 +141,11 @@ func (object *Char) TableName() string {
return dbPrefix + "_" + "chars"
}
func (object *Char) GetGiftToleranz() int {
//30+(Konstitution/2)
konstitution := object.GetAttributeValue("Ko")
return 30 + (konstitution / 2)
}
func (object *Char) ensureGameSystem() {
gs := GetGameSystem(object.GameSystemId, object.GameSystem)
if gs == nil {
@@ -259,7 +265,7 @@ func FindSharedCharList(userID uint) ([]CharList, error) {
var chars []CharList
gs := GetGameSystem(0, "midgard")
err := database.DB.Table("char_chars").
Select("char_chars.id, char_chars.name, char_chars.user_id, char_chars.rasse, char_chars.typ, char_chars.grad, char_chars.public, char_chars.game_system, char_chars.game_system_id, users.username as owner").
Select("char_chars.id, char_chars.name, char_chars.user_id, char_chars.rasse, char_chars.typ, char_chars.grad, char_chars.public, char_chars.game_system, char_chars.game_system_id, COALESCE(NULLIF(users.display_name, ''), users.username) as owner").
Joins("LEFT JOIN users ON char_chars.user_id = users.user_id").
Joins("INNER JOIN char_shares ON char_shares.character_id = char_chars.id").
Where("char_shares.user_id = ? AND (char_chars.game_system = ? OR char_chars.game_system_id = ?)", userID, gs.Name, gs.ID).
@@ -274,7 +280,7 @@ func FindPublicCharList() ([]CharList, error) {
var chars []CharList
gs := GetGameSystem(0, "midgard")
err := database.DB.Table("char_chars").
Select("char_chars.id, char_chars.name, char_chars.user_id, char_chars.rasse, char_chars.typ, char_chars.grad, char_chars.public, char_chars.game_system, char_chars.game_system_id, users.username as owner").
Select("char_chars.id, char_chars.name, char_chars.user_id, char_chars.rasse, char_chars.typ, char_chars.grad, char_chars.public, char_chars.game_system, char_chars.game_system_id, COALESCE(NULLIF(users.display_name, ''), users.username) as owner").
Joins("LEFT JOIN users ON char_chars.user_id = users.user_id").
Where("char_chars.public = ? AND (char_chars.game_system = ? OR char_chars.game_system_id = ?)", true, gs.Name, gs.ID).
Find(&chars).Error
@@ -289,7 +295,7 @@ func FindCharListByUserID(userID uint) ([]CharList, error) {
var chars []CharList
gs := GetGameSystem(0, "midgard")
err := database.DB.Table("char_chars").
Select("char_chars.id, char_chars.name, char_chars.user_id, char_chars.rasse, char_chars.typ, char_chars.grad, char_chars.public, char_chars.game_system, char_chars.game_system_id, users.username as owner").
Select("char_chars.id, char_chars.name, char_chars.user_id, char_chars.rasse, char_chars.typ, char_chars.grad, char_chars.public, char_chars.game_system, char_chars.game_system_id, COALESCE(NULLIF(users.display_name, ''), users.username) as owner").
Joins("LEFT JOIN users ON char_chars.user_id = users.user_id").
Where("char_chars.user_id = ? AND (char_chars.game_system = ? OR char_chars.game_system_id = ?)", userID, gs.Name, gs.ID).
Find(&chars).Error
+38 -1
View File
@@ -13,8 +13,11 @@ import (
func setupCharacterTestDB(t *testing.T) {
database.SetupTestDB()
err := user.MigrateStructure()
require.NoError(t, err, "Failed to migrate user structure")
// Migrate structures
err := MigrateStructure()
err = MigrateStructure()
require.NoError(t, err, "Failed to migrate database structure")
// Clean up any existing test data
@@ -355,6 +358,40 @@ func TestFindCharListByUserID_Success(t *testing.T) {
}
}
func TestFindPublicCharList_UsesOwnerDisplayName(t *testing.T) {
setupCharacterTestDB(t)
displayOwner := &user.User{
Username: "display-owner",
DisplayName: "Display Owner",
PasswordHash: "hash",
Email: "display-owner@example.com",
}
err := displayOwner.Create()
require.NoError(t, err, "User creation should succeed")
char := createTestChar("Public Char With Display Name")
char.UserID = displayOwner.UserID
char.Public = true
err = char.Create()
require.NoError(t, err, "Character creation should succeed")
publicChars, err := FindPublicCharList()
require.NoError(t, err, "FindPublicCharList should succeed")
var found *CharList
for i := range publicChars {
if publicChars[i].ID == char.ID {
found = &publicChars[i]
break
}
}
require.NotNil(t, found, "Created public character should be present in list")
assert.Equal(t, displayOwner.DisplayName, found.Owner, "Owner should use display name")
}
// =============================================================================
// Tests for Eigenschaft struct
// =============================================================================
+2 -2
View File
@@ -1,7 +1,7 @@
package transfer
import (
"bamort/config"
"bamort/appsystem"
"bamort/database"
"bamort/models"
"bamort/user"
@@ -90,7 +90,7 @@ func ExportDatabase(exportDir string) (*ExportResult, error) {
}
export := DatabaseExport{
Version: config.GetVersion(),
Version: appsystem.GetVersion(),
Timestamp: time.Now(),
}
+6 -3
View File
@@ -27,6 +27,7 @@ func ListUsers(c *gin.Context) {
for i := range users {
users[i].PasswordHash = ""
users[i].ResetPwHash = nil
users[i].DisplayName = users[i].DisplayNameOrUsername()
}
logger.Info("Successfully fetched %d users", len(users))
@@ -77,6 +78,7 @@ func GetUser(c *gin.Context) {
// Remove sensitive data
user.PasswordHash = ""
user.ResetPwHash = nil
user.DisplayName = user.DisplayNameOrUsername()
logger.Info("Successfully fetched user: %s (ID: %d)", user.Username, user.UserID)
c.JSON(http.StatusOK, user)
@@ -135,9 +137,10 @@ func UpdateUserRole(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "User role updated successfully",
"user": gin.H{
"id": user.UserID,
"username": user.Username,
"role": user.Role,
"id": user.UserID,
"username": user.Username,
"display_name": user.DisplayNameOrUsername(),
"role": user.Role,
},
})
}
+54 -3
View File
@@ -14,6 +14,7 @@ import (
"net/http"
"strconv"
"strings"
"unicode/utf8"
"github.com/gin-gonic/gin"
)
@@ -414,9 +415,10 @@ func ValidateResetToken(c *gin.Context) {
logger.Debug("Reset-Token gültig für Benutzer: %s", user.Username)
c.JSON(http.StatusOK, gin.H{
"valid": true,
"username": user.Username,
"expires": user.ResetPwHashExpires,
"valid": true,
"username": user.Username,
"display_name": user.DisplayNameOrUsername(),
"expires": user.ResetPwHashExpires,
})
}
@@ -443,6 +445,7 @@ func GetUserProfile(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"id": user.UserID,
"username": user.Username,
"display_name": user.DisplayNameOrUsername(),
"email": user.Email,
"role": user.Role,
"preferred_language": user.PreferredLanguage,
@@ -605,3 +608,51 @@ func UpdateLanguage(c *gin.Context) {
"language": user.PreferredLanguage,
})
}
// UpdateDisplayName Handler to update user's display name
func UpdateDisplayName(c *gin.Context) {
logger.Debug("Starte Anzeigenamen-Aktualisierung...")
userID, exists := c.Get("userID")
if !exists {
logger.Error("Benutzer-ID nicht im Context gefunden")
respondWithError(c, http.StatusUnauthorized, "Unauthorized")
return
}
var input struct {
DisplayName string `json:"display_name"`
}
if err := c.ShouldBindJSON(&input); err != nil {
logger.Error("Fehler beim Parsen des Anzeigenamens: %s", err.Error())
respondWithError(c, http.StatusBadRequest, "Display name is required")
return
}
if utf8.RuneCountInString(input.DisplayName) > 30 {
logger.Warn("Anzeigename zu lang: %d Zeichen", utf8.RuneCountInString(input.DisplayName))
respondWithError(c, http.StatusBadRequest, "Display name must be at most 30 characters")
return
}
var user User
if err := user.FirstId(userID.(uint)); err != nil {
logger.Error("Benutzer mit ID %v nicht gefunden: %s", userID, err.Error())
respondWithError(c, http.StatusNotFound, "User not found")
return
}
user.DisplayName = input.DisplayName
if err := user.Save(); err != nil {
logger.Error("Fehler beim Speichern des Anzeigenamens für Benutzer %s: %s", user.Username, err.Error())
respondWithError(c, http.StatusInternalServerError, "Failed to update display name")
return
}
logger.Info("Anzeigename erfolgreich aktualisiert für Benutzer: %s (ID: %d)", user.Username, user.UserID)
c.JSON(http.StatusOK, gin.H{
"message": "Display name updated successfully",
"display_name": user.DisplayNameOrUsername(),
})
}
+117
View File
@@ -9,6 +9,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"bamort/database"
@@ -31,6 +32,83 @@ func setupTestEnvironment(t *testing.T) {
})
}
func TestUpdateDisplayName(t *testing.T) {
setupHandlerTestEnvironment(t)
t.Run("Success - Update display name", func(t *testing.T) {
user := createTestUser(t, "displayuser", "password123", "display@test.com")
requestData := map[string]interface{}{
"display_name": "Neuer Anzeigename",
}
requestBody, _ := json.Marshal(requestData)
req, _ := http.NewRequest("PUT", "/api/user/display-name", bytes.NewBuffer(requestBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
c.Set("userID", user.UserID)
UpdateDisplayName(c)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "Display name updated successfully", response["message"])
assert.Equal(t, "Neuer Anzeigename", response["display_name"])
var updated User
err = updated.FirstId(user.UserID)
assert.NoError(t, err)
assert.Equal(t, "Neuer Anzeigename", updated.DisplayName)
})
t.Run("Failure - Display name too long", func(t *testing.T) {
user := createTestUser(t, "displayuser2", "password123", "display2@test.com")
requestData := map[string]interface{}{
"display_name": strings.Repeat("a", 31),
}
requestBody, _ := json.Marshal(requestData)
req, _ := http.NewRequest("PUT", "/api/user/display-name", bytes.NewBuffer(requestBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
c.Set("userID", user.UserID)
UpdateDisplayName(c)
assert.Equal(t, http.StatusBadRequest, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "Display name must be at most 30 characters", response["error"])
})
t.Run("Failure - No user ID in context", func(t *testing.T) {
requestData := map[string]interface{}{
"display_name": "Any",
}
requestBody, _ := json.Marshal(requestData)
req, _ := http.NewRequest("PUT", "/api/user/display-name", bytes.NewBuffer(requestBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
UpdateDisplayName(c)
assert.Equal(t, http.StatusUnauthorized, w.Code)
})
}
// setupHandlerTestEnvironment sets up the test environment for handler tests
func setupHandlerTestEnvironment(t *testing.T) {
setupTestEnvironment(t)
@@ -89,6 +167,45 @@ func TestGetUserProfile(t *testing.T) {
assert.Equal(t, float64(testUser.UserID), response["id"])
})
t.Run("Success - Profile uses display name with fallback", func(t *testing.T) {
userWithDisplay := createTestUser(t, "profiledisplay", "password123", "profiledisplay@test.com")
req, _ := http.NewRequest("GET", "/api/user/profile", nil)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = req
c.Set("userID", userWithDisplay.UserID)
GetUserProfile(c)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, userWithDisplay.Username, response["display_name"])
userWithDisplay.DisplayName = "Profile Display"
require.NoError(t, userWithDisplay.Save())
req2, _ := http.NewRequest("GET", "/api/user/profile", nil)
w2 := httptest.NewRecorder()
c2, _ := gin.CreateTestContext(w2)
c2.Request = req2
c2.Set("userID", userWithDisplay.UserID)
GetUserProfile(c2)
assert.Equal(t, http.StatusOK, w2.Code)
var response2 map[string]interface{}
err = json.Unmarshal(w2.Body.Bytes(), &response2)
assert.NoError(t, err)
assert.Equal(t, userWithDisplay.DisplayName, response2["display_name"])
})
t.Run("Failure - No user ID in context", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/api/user/profile", nil)
w := httptest.NewRecorder()
+17
View File
@@ -3,6 +3,7 @@ package user
import (
"bamort/database"
"fmt"
"strings"
"time"
"gorm.io/gorm"
@@ -18,6 +19,7 @@ const (
type User struct {
UserID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"unique" json:"username"`
DisplayName string `gorm:"not null;default:''" json:"display_name"`
PasswordHash string `json:"password"`
Email string `gorm:"unique" json:"email"`
Role string `gorm:"default:standard" json:"role"`
@@ -33,6 +35,10 @@ func (object *User) Create() error {
return fmt.Errorf("database connection is nil")
}
if strings.TrimSpace(object.DisplayName) == "" {
object.DisplayName = object.Username
}
err := database.DB.Transaction(func(tx *gorm.DB) error {
// Save the User record
if err := tx.Create(&object).Error; err != nil {
@@ -75,6 +81,10 @@ func (object *User) Save() error {
return fmt.Errorf("database connection is nil")
}
if strings.TrimSpace(object.DisplayName) == "" {
object.DisplayName = object.Username
}
err := database.DB.Save(&object).Error
if err != nil {
// User found
@@ -154,3 +164,10 @@ func (u *User) IsStandardUser() bool {
func ValidateRole(role string) bool {
return role == RoleStandardUser || role == RoleMaintainer || role == RoleAdmin
}
func (u *User) DisplayNameOrUsername() string {
if strings.TrimSpace(u.DisplayName) != "" {
return u.DisplayName
}
return u.Username
}
+37 -1
View File
@@ -39,6 +39,21 @@ func TestUserRoleDefaults(t *testing.T) {
assert.Equal(t, RoleStandardUser, user.Role, "New users should have standard role")
}
// TestUserDisplayNameDefaultsToUsername ensures a user's display name falls back to the username
func TestUserDisplayNameDefaultsToUsername(t *testing.T) {
setupRoleTestEnvironment(t)
user := &User{
Username: "display_user",
PasswordHash: "hashedpw",
Email: "display@example.com",
}
err := user.Create()
require.NoError(t, err, "Should create user")
assert.Equal(t, user.Username, user.DisplayName, "DisplayName should default to username")
}
// TestRoleValidation tests role validation
func TestRoleValidation(t *testing.T) {
assert.True(t, ValidateRole(RoleStandardUser), "standard should be valid")
@@ -75,7 +90,7 @@ func TestListUsers(t *testing.T) {
setupRoleTestEnvironment(t)
// Create test users
admin := &User{Username: "listadmin", PasswordHash: "hash", Email: "listadmin@test.com", Role: RoleAdmin}
admin := &User{Username: "listadmin", PasswordHash: "hash", Email: "listadmin@test.com", Role: RoleAdmin, DisplayName: "List Admin"}
require.NoError(t, admin.Create())
standardUser := &User{Username: "listuser", PasswordHash: "hash", Email: "listuser@test.com", Role: RoleStandardUser}
@@ -93,6 +108,27 @@ func TestListUsers(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code, "Admin should access list")
var response []map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
var adminEntry map[string]interface{}
var standardEntry map[string]interface{}
for _, entry := range response {
switch entry["username"] {
case admin.Username:
adminEntry = entry
case standardUser.Username:
standardEntry = entry
}
}
require.NotNil(t, adminEntry, "admin should be present in response")
require.NotNil(t, standardEntry, "standard user should be present in response")
assert.Equal(t, admin.DisplayName, adminEntry["display_name"], "admin display name should be returned")
assert.Equal(t, standardUser.Username, standardEntry["display_name"], "standard user should fall back to username when display name is empty")
// Test standard user access (should fail)
router2 := gin.Default()
router2.GET("/users", func(c *gin.Context) {
+1
View File
@@ -10,6 +10,7 @@ func RegisterRoutes(r *gin.RouterGroup) {
{
// Protected routes - require authentication
userGroup.GET("/profile", GetUserProfile)
userGroup.PUT("/display-name", UpdateDisplayName)
userGroup.PUT("/email", UpdateEmail)
userGroup.PUT("/password", UpdatePassword)
userGroup.PUT("/language", UpdateLanguage)
+1 -1
View File
@@ -1,6 +1,6 @@
# Frontend Version Management
## Current Version: 0.2.1
## Current Version: 0.2.2
The frontend version is managed independently from the backend.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bamort-frontend",
"version": "0.2.1",
"version": "0.2.2",
"private": true,
"license": "SEE LICENSE IN LICENSE",
"type": "module",
@@ -6,7 +6,16 @@
<div class="form-row">
<!-- 1. Name -->
<div class="form-group">
<label for="name">{{ $t('characters.basicInfo.characterName') }} {{ $t('characters.basicInfo.required') }}</label>
<label for="name">{{ $t('characters.basicInfo.characterName') }} {{ $t('characters.basicInfo.required') }}
<span
class="help-icon"
:title="$t('characters.basicInfo.characterNameHelp')"
role="img"
:aria-label="$t('characters.basicInfo.characterNameHelp')"
>
?
</span>
</label>
<input
id="name"
v-model="formData.name"
@@ -30,7 +39,17 @@
<!-- 3. Glaube -->
<div class="form-group">
<label for="glaube">{{ $t('characters.basicInfo.religion') }}</label>
<label for="glaube">
{{ $t('characters.basicInfo.religion') }}
<span
class="help-icon"
:title="$t('characters.basicInfo.religionHelp')"
role="img"
:aria-label="$t('characters.basicInfo.religionHelp')"
>
?
</span>
</label>
<div class="belief-search">
<input
id="glaube"
@@ -530,4 +549,25 @@ export default {
color: #666;
margin-top: 15px;
}
.help-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
margin-left: 6px;
border: 1px solid #999;
border-radius: 50%;
font-size: 12px;
line-height: 1;
/*cursor: help;*/
background: #f5f5f5;
color: #555;
}
.help-icon:hover {
background: #e0e0e0;
color: #222;
}
</style>
+1 -1
View File
@@ -9,7 +9,7 @@
<button v-if="isOwner" @click="showVisibilityDialog = true" class="export-button-small" :title="$t('visibility.title')">
{{ character.public ? '🌐' : '🔒' }}
</button>
<h2>{{ $t('char') }}: {{ character.name }} ({{ $t(currentView) }})</h2>
<h2>{{ character.name }} {{$t('characters.list.class')}}: {{ character.typ }} {{$t('characters.list.grade') }}: {{ character.grad }} <!--({{ $t(currentView) }})--></h2>
</div>
</div>
+58 -18
View File
@@ -36,6 +36,23 @@
<!-- Character Information -->
<div class="character-info">
<div class="info-section">
<p>
<strong>{{ $t('char') }}:</strong>
<span
v-if="editingProp !== 'name'"
@dblclick="startEditProp('name', character.name)"
class="editable-prop"
>{{ character.name || '-' }}</span>
<input
v-else
v-model="editPropValue"
@blur="saveProp('name')"
@keyup.enter="saveProp('name')"
@keyup.esc="cancelEditProp"
ref="propInput"
class="prop-input"
/>
</p>
<p>
<span
v-if="editingProp !== 'typ'"
@@ -167,7 +184,17 @@
@dblclick="startEditProp('glaube', character.glaube)"
class="editable-prop"
>{{ character.glaube || '-' }}</span>
<select v-else v-model="editPropValue" @blur="saveProp('glaube')" @keyup.enter="saveProp('glaube')" @keyup.esc="cancelEditProp" ref="propInput" class="prop-input">
<select
v-else
v-model="editPropValue"
multiple
@blur="saveProp('glaube')"
@keyup.enter="saveProp('glaube')"
@keyup.esc="cancelEditProp"
ref="propInput"
class="prop-input multi-select"
size="8"
>
<option v-for="option in getSelectOptions('glaube')" :key="option" :value="option">{{ option }}</option>
</select>
</p>
@@ -190,8 +217,12 @@
<div v-else>Loading character data...</div>
</template>
<style>
/* All styles moved to main.css */
<style scoped>
.multi-select {
min-width: 200px;
max-height: 180px;
overflow-y: auto;
}
</style>
<script>
@@ -259,14 +290,16 @@ export default {
this.$emit('character-updated')
},
getStat(path) {
/*
if (path === 'git') {
// Todo: calculate poison tolerance based on character data
return '64!'
}
*/
return path.split('.').reduce((obj, key) => obj?.[key], this.character) ?? '-'
},
startEdit(index, path) {
if (path === 'git') return
//if (path === 'git') return
this.editingIndex = index
this.editValue = this.getStat(path)
@@ -315,18 +348,22 @@ export default {
const selectFields = ['gender', 'rasse', 'origin', 'social_class', 'glaube', 'hand', 'spezialisierung']
if (selectFields.includes(prop)) {
this.loadDatasheetOptions()
type = 'select'
type = prop === 'glaube' ? 'multi-select' : 'select'
}
this.editingProp = prop
this.editPropValue = value || ''
if (prop === 'glaube') {
this.editPropValue = value ? value.split(',').map(v => v.trim()).filter(Boolean) : []
} else {
this.editPropValue = value || ''
}
this.editPropType = type
this.$nextTick(() => {
if (this.$refs.propInput) {
const input = Array.isArray(this.$refs.propInput) ? this.$refs.propInput[0] : this.$refs.propInput
if (input) {
input.focus()
if (type !== 'select') {
if (type !== 'select' && type !== 'multi-select') {
input.select()
}
}
@@ -335,6 +372,17 @@ export default {
},
async saveProp(prop) {
if (this.editingProp === null) return
let newValue = this.editPropValue
if (this.editPropType === 'number') {
newValue = parseInt(this.editPropValue)
if (isNaN(newValue)) {
this.cancelEditProp()
return
}
} else if (this.editPropType === 'multi-select') {
newValue = Array.isArray(this.editPropValue) ? this.editPropValue.join(', ') : ''
}
// Update the character object directly
const pathParts = prop.split('.')
let obj = this.character
@@ -345,19 +393,11 @@ export default {
obj = obj[pathParts[i]]
}
obj[pathParts[pathParts.length - 1]] = newValue
// Save to backend
await API.put(`/api/characters/${this.character.id}`, this.character)
this.$emit('character-updated',this.character)
let newValue = this.editPropValue
if (this.editPropType === 'number') {
newValue = parseInt(this.editPropValue)
if (isNaN(newValue)) {
this.cancelEditProp()
return
}
}
this.$emit('character-updated', this.character)
try {
this.$emit('update-property', prop, newValue)
+19 -4
View File
@@ -34,6 +34,11 @@ import SpellView from "./maintenance/SpellView.vue"; // Component for character
import EquipmentView from "./maintenance/EquipmentView.vue"; // Component for character equipment
import WeaponView from "./maintenance/WeaponView.vue"; // Component for character history
import WeaponSkillView from "./maintenance/WeaponSkillView.vue"; // Component for character equipment
import BelieveView from "./maintenance/BelieveView.vue"; // Component for believes maintenance
import GameSystemView from "./maintenance/GameSystemView.vue";
import LitSourceView from "./maintenance/LitSourceView.vue";
import MiscLookupView from "./maintenance/MiscLookupView.vue";
import SkillImprovementCostView from "./maintenance/SkillImprovementCostView.vue";
export default {
@@ -45,6 +50,11 @@ export default {
EquipmentView,
WeaponView,
WeaponSkillView,
BelieveView,
GameSystemView,
LitSourceView,
MiscLookupView,
SkillImprovementCostView,
},
data() {
return {
@@ -63,10 +73,15 @@ export default {
lastView: "SkillView",
menus: [
{ id: 0, name: "skill", component: "SkillView" },
{ id: 2, name: "spell", component: "SpellView" },
{ id: 3, name: "equipment", component: "EquipmentView" },
{ id: 1, name: "weapon", component: "WeaponView" },
{ id: 1, name: "weaponskill", component: "WeaponSkillView" },
{ id: 1, name: "spell", component: "SpellView" },
{ id: 2, name: "equipment", component: "EquipmentView" },
{ id: 3, name: "weapon", component: "WeaponView" },
{ id: 4, name: "weaponskill", component: "WeaponSkillView" },
{ id: 5, name: "believe", component: "BelieveView" },
{ id: 6, name: "gamesystem", component: "GameSystemView" },
{ id: 7, name: "litsource", component: "LitSourceView" },
{ id: 8, name: "misc", component: "MiscLookupView" },
{ id: 9, name: "skillimprovement", component: "SkillImprovementCostView" },
],
};
@@ -32,8 +32,8 @@
<div v-else>
<div class="page-header">
<h2>Neues Passwort setzen</h2>
<p style="color: #666; font-size: 0.9em; margin-top: 10px;" v-if="userInfo.username">
Für Benutzer: <strong>{{ userInfo.username }}</strong>
<p style="color: #666; font-size: 0.9em; margin-top: 10px;" v-if="userInfo.display_name || userInfo.username">
Für Benutzer: <strong>{{ userInfo.display_name || userInfo.username }}</strong>
</p>
</div>
@@ -155,6 +155,7 @@ export default {
this.isValidToken = response.data.valid
this.userInfo = {
username: response.data.username,
display_name: response.data.display_name,
expires: response.data.expires
}
+3 -2
View File
@@ -72,7 +72,7 @@
@click="toggleUser(user.user_id)"
>
<div class="user-info">
<span class="user-name">{{ user.username }}</span>
<span class="user-name">{{ user.display_name || user.username }}</span>
<span class="user-email">{{ user.email }}</span>
</div>
</div>
@@ -161,6 +161,7 @@ export default {
}
const query = this.searchQuery.toLowerCase()
return users.filter(user =>
(user.display_name && user.display_name.toLowerCase().includes(query)) ||
user.username.toLowerCase().includes(query) ||
user.email.toLowerCase().includes(query)
)
@@ -228,7 +229,7 @@ export default {
getUserName(userId) {
const user = this.availableUsers.find(u => u.user_id === userId)
return user ? user.username : 'Unknown'
return user ? (user.display_name || user.username) : 'Unknown'
},
getUserEmail(userId) {
@@ -0,0 +1,300 @@
<template>
<div class="header-section">
<h2>{{ $t('maintenance') }} - {{ $t('believe.title') }}</h2>
<div class="search-box">
<input
v-model="searchTerm"
type="text"
:placeholder="$t('search')"
/>
</div>
</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('believe.id') }}</th>
<th class="cd-table-header">{{ $t('believe.name') }}</th>
<th class="cd-table-header">{{ $t('believe.description') }}</th>
<th class="cd-table-header">{{ $t('believe.source') }}</th>
<th class="cd-table-header">{{ $t('believe.page') }}</th>
<th class="cd-table-header">{{ $t('believe.system') }}</th>
<th class="cd-table-header"></th>
</tr>
</thead>
<tbody>
<tr v-if="isLoading">
<td colspan="7">{{ $t('common.loading') }}</td>
</tr>
<template v-for="believe in filteredBelieves" :key="believe.id">
<tr v-if="editingId !== believe.id">
<td>{{ believe.id }}</td>
<td>{{ believe.name }}</td>
<td>{{ believe.beschreibung || '-' }}</td>
<td>{{ getSourceCode(believe.source_id) || '-' }}</td>
<td>{{ believe.page_number || '-' }}</td>
<td>{{ getSystemCodeById(believe.game_system_id, believe.game_system) || '-' }}</td>
<td>
<button @click="startEdit(believe)">{{ $t('believe.edit') }}</button>
</td>
</tr>
<tr v-else>
<td>{{ believe.id }}</td>
<td colspan="6">
<div class="edit-form">
<div class="edit-row">
<label>{{ $t('believe.name') }}</label>
<input v-model="editedItem.name" />
</div>
<div class="edit-row">
<label>{{ $t('believe.description') }}</label>
<input v-model="editedItem.beschreibung" />
</div>
<div class="edit-row">
<label>{{ $t('believe.source') }}</label>
<select v-model="editedItem.sourceCode">
<option value="">-</option>
<option v-for="source in sources" :key="source.id" :value="source.code">
{{ source.code }}
</option>
</select>
<label class="inline-label">{{ $t('believe.page') }}</label>
<input v-model.number="editedItem.page_number" type="number" min="0" />
</div>
<div class="edit-row">
<label>{{ $t('believe.system') }}</label>
<select v-model.number="selectedSystemId">
<option value="">-</option>
<option v-for="system in systemOptions" :key="system.id" :value="system.id">
{{ system.label }}
</option>
</select>
</div>
<div class="edit-actions">
<button
class="btn-primary"
:disabled="isSaving"
@click="saveEdit"
>
<span v-if="!isSaving">{{ $t('believe.save') }}</span>
<span v-else>{{ $t('believe.saving') }}</span>
</button>
<button
class="btn-cancel"
:disabled="isSaving"
@click="cancelEdit"
>
{{ $t('believe.cancel') }}
</button>
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<style scoped>
/* Uses shared maintenance styles */
.error-box {
margin: 10px 0;
padding: 10px 12px;
background: #ffe3e3;
color: #8a1c1c;
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-row label {
font-weight: 600;
}
.edit-row input,
.edit-row select {
padding: 6px 10px;
border: 1px solid #dee2e6;
border-radius: 6px;
}
.edit-actions {
display: flex;
gap: 10px;
}
.inline-label {
margin-left: 10px;
}
</style>
<script>
import API from '../../utils/api'
import {
findSystemIdByCode,
getSourceCode,
getSystemCodeById,
loadGameSystems as fetchGameSystems,
buildSystemOptions,
} from '../../utils/maintenanceGameSystems'
export default {
name: "BelieveView",
props: {
mdata: {
type: Object,
required: false,
default: () => ({})
}
},
data() {
return {
believes: [],
sources: [],
editingId: null,
editedItem: null,
gameSystems: [],
selectedSystemId: null,
isLoading: false,
isSaving: false,
error: '',
searchTerm: ''
}
},
async created() {
await this.loadGameSystems()
await this.loadBelieves()
},
computed: {
filteredBelieves() {
const term = this.searchTerm.trim().toLowerCase()
const filtered = term
? this.believes.filter(believe => {
const name = (believe.name || '').toLowerCase()
const desc = (believe.beschreibung || '').toLowerCase()
return name.includes(term) || desc.includes(term)
})
: this.believes
return [...filtered].sort((a, b) => (a.name || '').localeCompare(b.name || ''))
},
systemOptions() {
return buildSystemOptions(this.gameSystems)
}
},
methods: {
async loadGameSystems() {
try {
this.gameSystems = await fetchGameSystems()
} catch (err) {
console.error('Failed to load game systems:', err)
this.error = err.response?.data?.error || err.message
}
},
async loadBelieves() {
this.isLoading = true
this.error = ''
try {
const response = await API.get('/api/maintenance/gsm-believes')
this.believes = response.data?.believes || []
this.sources = response.data?.sources || []
} catch (err) {
console.error('Failed to load believes:', err)
this.error = err.response?.data?.error || err.message
} finally {
this.isLoading = false
}
},
getSourceCode(sourceId) {
return getSourceCode(this.sources, sourceId)
},
getSystemCodeById(systemId, fallback = '') {
return getSystemCodeById(this.gameSystems, systemId, fallback)
},
startEdit(believe) {
this.editingId = believe.id
this.editedItem = {
...believe,
sourceCode: this.getSourceCode(believe.source_id)
}
this.selectedSystemId = believe.game_system_id ?? this.findSystemIdByCode(believe.game_system)
},
cancelEdit() {
this.editingId = null
this.editedItem = null
this.selectedSystemId = null
},
findSystemIdByCode(code) {
return findSystemIdByCode(this.gameSystems, code)
},
async saveEdit() {
if (!this.editedItem || !this.editingId) {
return
}
const trimmedName = (this.editedItem.name || '').trim()
if (!trimmedName) {
alert(this.$t('believe.nameRequired'))
return
}
const selectedSource = this.sources.find(src => src.code === this.editedItem.sourceCode)
const selectedSystem = this.gameSystems.find(gs => gs.id === this.selectedSystemId)
const payload = {
name: trimmedName,
beschreibung: this.editedItem.beschreibung || '',
source_id: selectedSource ? selectedSource.id : null,
page_number: this.editedItem.page_number || 0,
game_system_id: selectedSystem ? selectedSystem.id : null,
game_system: selectedSystem ? selectedSystem.code : '',
}
this.isSaving = true
try {
const response = await API.put(`/api/maintenance/gsm-believes/${this.editingId}`, payload)
const updated = response.data
const sourceCode = this.getSourceCode(updated.source_id)
const gameSystemCode = selectedSystem ? selectedSystem.code : updated.game_system
const gameSystemId = selectedSystem ? selectedSystem.id : (updated.game_system_id ?? null)
const idx = this.believes.findIndex(b => b.id === this.editingId)
if (idx !== -1) {
this.believes.splice(idx, 1, { ...updated, source_code: sourceCode, game_system: gameSystemCode, game_system_id: gameSystemId })
}
this.cancelEdit()
} catch (err) {
console.error('Failed to save believe:', err)
this.error = err.response?.data?.error || err.message
} finally {
this.isSaving = false
}
}
}
}
</script>
@@ -62,7 +62,7 @@
<td>{{ dtaItem.beschreibung || '-' }}</td>
<td>{{ formatQuelle(dtaItem) }}</td>
<td><input type="checkbox" :checked="dtaItem.personal_item" disabled /></td>
<td>{{ dtaItem.system || 'midgard' }}</td>
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.system || 'midgard') }}</td>
<td>
<button @click="startEdit(index)">Edit</button>
</td>
@@ -115,7 +115,12 @@
</div>
<div class="edit-field">
<label>{{ $t('equipment.system') }}:</label>
<input v-model="editedItem.system" style="width:100px;" />
<select v-model.number="selectedSystemId" style="width:140px;">
<option value="">-</option>
<option v-for="system in systemOptions" :key="system.id" :value="system.id">
{{ system.label }}
</option>
</select>
</div>
</div>
@@ -180,6 +185,13 @@
<script>
import API from '../../utils/api'
import {
findSystemIdByCode,
getSourceCode,
getSystemCodeById,
loadGameSystems as fetchGameSystems,
buildSystemOptions,
} from '../../utils/maintenanceGameSystems'
export default {
name: "EquipmentView",
props: {
@@ -202,11 +214,16 @@ export default {
filterPersonalItem: '',
filterQuelle: '',
enhancedEquipment: [],
availableSources: []
availableSources: [],
gameSystems: [],
selectedSystemId: null
}
},
async created() {
await this.loadEnhancedEquipment()
await Promise.all([
this.loadGameSystems(),
this.loadEnhancedEquipment()
])
},
computed: {
availableQuellen() {
@@ -266,9 +283,19 @@ export default {
? aValue.localeCompare(bValue)
: bValue.localeCompare(aValue);
});
},
systemOptions() {
return buildSystemOptions(this.gameSystems)
}
},
methods: {
async loadGameSystems() {
try {
this.gameSystems = await fetchGameSystems()
} catch (error) {
console.error('Failed to load game systems:', error)
}
},
async loadEnhancedEquipment() {
try {
const response = await API.get('/api/maintenance/equipment-enhanced')
@@ -284,17 +311,21 @@ export default {
...equipment,
sourceCode: this.getSourceCode(equipment.source_id)
}
this.selectedSystemId = equipment.game_system_id ?? this.findSystemIdByCode(equipment.system)
this.editingIndex = index
},
async saveEdit(index) {
try {
// Find source ID from code
const source = this.availableSources.find(s => s.code === this.editedItem.sourceCode)
const selectedSystem = this.gameSystems.find(gs => gs.id === this.selectedSystemId)
const updateData = {
...this.editedItem,
source_id: source ? source.id : null,
page_number: this.editedItem.page_number || 0
page_number: this.editedItem.page_number || 0,
system: selectedSystem ? selectedSystem.code : (this.editedItem.system || ''),
game_system_id: selectedSystem ? selectedSystem.id : (this.editedItem.game_system_id ?? null)
}
const response = await API.put(
@@ -310,6 +341,7 @@ export default {
this.editingIndex = -1
this.editedItem = null
this.selectedSystemId = null
} catch (error) {
console.error('Failed to save equipment:', error)
alert('Failed to save equipment: ' + (error.response?.data?.error || error.message))
@@ -318,6 +350,10 @@ export default {
cancelEdit() {
this.editingIndex = -1;
this.editedItem = null;
this.selectedSystemId = null;
},
findSystemIdByCode(code) {
return findSystemIdByCode(this.gameSystems, code)
},
sortBy(field) {
if (this.sortField === field) {
@@ -343,15 +379,16 @@ export default {
return equipment.quelle || '-'
},
getSourceCode(sourceId) {
if (!sourceId || !this.availableSources.length) return ''
const source = this.availableSources.find(s => s.id === sourceId)
return source ? source.code : ''
return getSourceCode(this.availableSources, sourceId)
},
clearFilters() {
this.searchTerm = ''
this.filterPersonalItem = ''
this.filterQuelle = ''
},
getSystemCodeById(systemId, fallback = '') {
return getSystemCodeById(this.gameSystems, systemId, fallback)
},
async handleEquipmentUpdate({ index, equipment }) {
try {
const response = await API.put(
@@ -0,0 +1,174 @@
<template>
<div class="header-section">
<h2>{{ $t('maintenance') }} - {{ $t('gamesystem.title') }}</h2>
<div class="search-box">
<input v-model="searchTerm" type="text" :placeholder="$t('search')" />
</div>
</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('gamesystem.id') }}</th>
<th class="cd-table-header">{{ $t('gamesystem.code') }}</th>
<th class="cd-table-header">{{ $t('gamesystem.name') }}</th>
<th class="cd-table-header">{{ $t('gamesystem.description') }}</th>
<th class="cd-table-header">{{ $t('gamesystem.active') }}</th>
<th class="cd-table-header"></th>
</tr>
</thead>
<tbody>
<tr v-if="isLoading">
<td colspan="6">{{ $t('common.loading') }}</td>
</tr>
<template v-for="gs in filteredSystems" :key="gs.id">
<tr v-if="editingId !== gs.id">
<td>{{ gs.id }}</td>
<td>{{ gs.code }}</td>
<td>{{ gs.name }}</td>
<td>{{ gs.description || '-' }}</td>
<td><input type="checkbox" :checked="gs.is_active" disabled /></td>
<td><button @click="startEdit(gs)">{{ $t('gamesystem.edit') }}</button></td>
</tr>
<tr v-else>
<td>{{ gs.id }}</td>
<td>{{ gs.code }}</td>
<td colspan="4">
<div class="edit-form">
<div class="edit-row">
<label>{{ $t('gamesystem.name') }}</label>
<input v-model="editedItem.name" />
</div>
<div class="edit-row">
<label>{{ $t('gamesystem.description') }}</label>
<input v-model="editedItem.description" />
</div>
<div class="edit-row">
<label>{{ $t('gamesystem.active') }}</label>
<input type="checkbox" v-model="editedItem.is_active" />
</div>
<div class="edit-actions">
<button class="btn-primary" :disabled="isSaving" @click="saveEdit">
<span v-if="!isSaving">{{ $t('gamesystem.save') }}</span>
<span v-else>{{ $t('gamesystem.saving') }}</span>
</button>
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
{{ $t('gamesystem.cancel') }}
</button>
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<style scoped>
.error-box {
margin: 10px 0;
padding: 10px 12px;
background: #ffe3e3;
color: #8a1c1c;
border: 1px solid #f5c2c2;
border-radius: 6px;
}
.edit-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.edit-row {
display: flex;
gap: 10px;
align-items: center;
}
.edit-actions {
display: flex;
gap: 10px;
}
</style>
<script>
import API from '../../utils/api'
import { loadGameSystems as fetchGameSystems } from '../../utils/maintenanceGameSystems'
export default {
name: 'GameSystemView',
data() {
return {
systems: [],
editingId: null,
editedItem: null,
isLoading: false,
isSaving: false,
error: '',
searchTerm: '',
}
},
async created() {
await this.loadSystems()
},
computed: {
filteredSystems() {
const term = this.searchTerm.trim().toLowerCase()
const list = term
? this.systems.filter(gs =>
(gs.name || '').toLowerCase().includes(term) ||
(gs.code || '').toLowerCase().includes(term)
)
: this.systems
return [...list].sort((a, b) => (a.code || '').localeCompare(b.code || ''))
},
},
methods: {
async loadSystems() {
this.isLoading = true
this.error = ''
try {
this.systems = await fetchGameSystems()
} catch (err) {
console.error('Failed to load game systems:', err)
this.error = err.response?.data?.error || err.message
} finally {
this.isLoading = false
}
},
startEdit(gs) {
this.editingId = gs.id
this.editedItem = { ...gs }
},
cancelEdit() {
this.editingId = null
this.editedItem = null
},
async saveEdit() {
if (!this.editedItem) return
const payload = {
name: this.editedItem.name || '',
description: this.editedItem.description || '',
is_active: !!this.editedItem.is_active,
}
this.isSaving = true
try {
const resp = await API.put(`/api/maintenance/game-systems/${this.editingId}`, payload)
const idx = this.systems.findIndex(s => s.id === this.editingId)
if (idx !== -1) this.systems.splice(idx, 1, resp.data)
this.cancelEdit()
} catch (err) {
console.error('Failed to save game system:', err)
this.error = err.response?.data?.error || err.message
} finally {
this.isSaving = false
}
},
},
}
</script>
@@ -0,0 +1,277 @@
<template>
<div class="header-section">
<h2>{{ $t('maintenance') }} - {{ $t('litsource.title') }}</h2>
<div class="search-box">
<input v-model="searchTerm" type="text" :placeholder="$t('search')" />
</div>
<div class="search-box">
<select v-model.number="selectedSystemId" @change="handleGameSystemChange">
<option value="">{{ $t('gamesystem.title') }}</option>
<option v-for="system in systemOptions" :key="system.id" :value="system.id">
{{ system.label }}
</option>
</select>
</div>
</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('litsource.id') }}</th>
<th class="cd-table-header">{{ $t('litsource.code') }}</th>
<th class="cd-table-header">{{ $t('litsource.name') }}</th>
<th class="cd-table-header">{{ $t('litsource.fullName') }}</th>
<th class="cd-table-header">{{ $t('litsource.edition') }}</th>
<th class="cd-table-header">{{ $t('litsource.publisher') }}</th>
<th class="cd-table-header">{{ $t('litsource.year') }}</th>
<th class="cd-table-header">{{ $t('litsource.active') }}</th>
<th class="cd-table-header">{{ $t('litsource.core') }}</th>
<th class="cd-table-header"></th>
</tr>
</thead>
<tbody>
<tr v-if="isLoading">
<td colspan="10">{{ $t('common.loading') }}</td>
</tr>
<template v-for="src in filteredSources" :key="src.id">
<tr v-if="editingId !== src.id">
<td>{{ src.id }}</td>
<td>{{ src.code }}</td>
<td>{{ src.name }}</td>
<td>{{ src.full_name }}</td>
<td>{{ src.edition }}</td>
<td>{{ src.publisher }}</td>
<td>{{ src.publish_year }}</td>
<td><input type="checkbox" :checked="src.is_active" disabled /></td>
<td><input type="checkbox" :checked="src.is_core" disabled /></td>
<td><button @click="startEdit(src)">{{ $t('litsource.edit') }}</button></td>
</tr>
<tr v-else>
<td>{{ src.id }}</td>
<td>{{ src.code }}</td>
<td colspan="8">
<div class="edit-form">
<div class="edit-row">
<label>{{ $t('litsource.name') }}</label>
<input v-model="editedItem.name" />
</div>
<div class="edit-row">
<label>{{ $t('litsource.fullName') }}</label>
<input v-model="editedItem.full_name" />
</div>
<div class="edit-row">
<label>{{ $t('litsource.edition') }}</label>
<input v-model="editedItem.edition" />
<label class="inline-label">{{ $t('litsource.publisher') }}</label>
<input v-model="editedItem.publisher" />
<label class="inline-label">{{ $t('litsource.year') }}</label>
<input v-model.number="editedItem.publish_year" type="number" />
</div>
<div class="edit-row">
<label>{{ $t('litsource.description') }}</label>
<input v-model="editedItem.description" />
</div>
<div class="edit-row">
<label>{{ $t('litsource.active') }}</label>
<input type="checkbox" v-model="editedItem.is_active" />
<label class="inline-label">{{ $t('litsource.core') }}</label>
<input type="checkbox" v-model="editedItem.is_core" />
</div>
<div class="edit-row">
<label>{{ $t('gamesystem.title') }}</label>
<select v-model.number="selectedSystemId">
<option value="">-</option>
<option v-for="system in systemOptions" :key="system.id" :value="system.id">
{{ system.label }}
</option>
</select>
</div>
<div class="edit-actions">
<button class="btn-primary" :disabled="isSaving" @click="saveEdit">
<span v-if="!isSaving">{{ $t('litsource.save') }}</span>
<span v-else>{{ $t('litsource.saving') }}</span>
</button>
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
{{ $t('litsource.cancel') }}
</button>
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<style scoped>
.error-box {
margin: 10px 0;
padding: 10px 12px;
background: #ffe3e3;
color: #8a1c1c;
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>
import API from '../../utils/api'
import {
buildGameSystemParams,
findSystemById,
findSystemIdByCode,
loadGameSystems as fetchGameSystems,
buildSystemOptions,
} from '../../utils/maintenanceGameSystems'
export default {
name: 'LitSourceView',
data() {
return {
gameSystems: [],
currentGameSystem: null,
selectedSystemId: null,
sources: [],
editingId: null,
editedItem: null,
isLoading: false,
isSaving: false,
error: '',
searchTerm: '',
}
},
async created() {
await this.initialize()
},
computed: {
filteredSources() {
const term = this.searchTerm.trim().toLowerCase()
const list = term
? this.sources.filter(src =>
(src.name || '').toLowerCase().includes(term) ||
(src.code || '').toLowerCase().includes(term)
)
: this.sources
return [...list].sort((a, b) => (a.code || '').localeCompare(b.code || ''))
},
systemOptions() {
return buildSystemOptions(this.gameSystems)
},
},
methods: {
async initialize() {
this.error = ''
await this.loadGameSystems()
if (this.currentGameSystem) {
await this.loadSources()
}
},
async loadGameSystems() {
try {
const systems = await fetchGameSystems()
this.gameSystems = systems
const active = systems.find(s => s.is_active)
this.currentGameSystem = active || systems[0] || null
this.selectedSystemId = this.currentGameSystem ? this.currentGameSystem.id : null
} catch (err) {
console.error('Failed to load game systems:', err)
this.error = err.response?.data?.error || err.message
}
},
async loadSources() {
this.isLoading = true
this.error = ''
try {
const params = buildGameSystemParams(this.currentGameSystem)
const resp = await API.get('/api/maintenance/gsm-lit-sources', { params })
this.sources = resp.data?.sources || []
} catch (err) {
console.error('Failed to load sources:', err)
this.error = err.response?.data?.error || err.message
} finally {
this.isLoading = false
}
},
startEdit(src) {
this.editingId = src.id
this.editedItem = { ...src }
this.selectedSystemId = src.game_system_id
|| this.findSystemIdByCode(src.game_system)
|| this.currentGameSystem?.id
|| null
},
cancelEdit() {
this.editingId = null
this.editedItem = null
this.selectedSystemId = this.currentGameSystem ? this.currentGameSystem.id : null
},
handleGameSystemChange() {
const target = this.findSystemById(this.selectedSystemId)
this.currentGameSystem = target || this.currentGameSystem
if (this.currentGameSystem) {
this.loadSources()
}
},
findSystemById(id) {
return findSystemById(this.gameSystems, id)
},
findSystemIdByCode(code) {
return findSystemIdByCode(this.gameSystems, code)
},
async saveEdit() {
if (!this.editedItem) return
const targetSystem = this.findSystemById(this.selectedSystemId) || this.currentGameSystem
const payload = {
name: this.editedItem.name || '',
full_name: this.editedItem.full_name || '',
edition: this.editedItem.edition || '',
publisher: this.editedItem.publisher || '',
publish_year: this.editedItem.publish_year || 0,
description: this.editedItem.description || '',
is_active: !!this.editedItem.is_active,
is_core: !!this.editedItem.is_core,
game_system_id: targetSystem ? targetSystem.id : null,
game_system: targetSystem ? targetSystem.code : '',
}
this.isSaving = true
try {
const params = buildGameSystemParams(targetSystem)
const resp = await API.put(`/api/maintenance/gsm-lit-sources/${this.editingId}`, payload, { params })
const idx = this.sources.findIndex(s => s.id === this.editingId)
if (idx !== -1) this.sources.splice(idx, 1, resp.data)
this.cancelEdit()
} catch (err) {
console.error('Failed to save source:', err)
this.error = err.response?.data?.error || err.message
} finally {
this.isSaving = false
}
},
},
}
</script>
@@ -0,0 +1,284 @@
<template>
<div class="header-section">
<h2>{{ $t('maintenance') }} - {{ $t('misc.title') }}</h2>
<div class="search-box">
<input v-model="searchTerm" type="text" :placeholder="$t('search')" />
</div>
</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('misc.id') }}</th>
<th class="cd-table-header">{{ $t('misc.key') }}</th>
<th class="cd-table-header">{{ $t('misc.value') }}</th>
<th class="cd-table-header">{{ $t('misc.source') }}</th>
<th class="cd-table-header">{{ $t('misc.page') }}</th>
<th class="cd-table-header">{{ $t('misc.system') }}</th>
<th class="cd-table-header"></th>
</tr>
</thead>
<tbody>
<tr v-if="isLoading">
<td colspan="7">{{ $t('common.loading') }}</td>
</tr>
<template v-for="item in filteredItems" :key="item.id">
<tr v-if="editingId !== item.id">
<td>{{ item.id }}</td>
<td>{{ item.key }}</td>
<td>{{ item.value }}</td>
<td>{{ sourceCodeFor(item.source_id) }}</td>
<td>{{ item.page_number || '-' }}</td>
<td>{{ systemCodeFor(item.game_system_id, item.game_system) || '-' }}</td>
<td><button @click="startEdit(item)">{{ $t('misc.edit') }}</button></td>
</tr>
<tr v-else>
<td>{{ item.id }}</td>
<td colspan="6">
<div class="edit-form">
<div class="edit-row">
<label>{{ $t('misc.key') }}</label>
<select v-model="editedItem.key">
<option :value="''">-</option>
<option v-for="key in keyOptionsWithCurrent" :key="key" :value="key">{{ key }}</option>
</select>
<label class="inline-label">{{ $t('misc.value') }}</label>
<input v-model="editedItem.value" />
</div>
<div class="edit-row">
<label>{{ $t('misc.source') }}</label>
<select v-model.number="editedItem.source_id">
<option :value="null">-</option>
<option v-for="src in sourceOptions" :key="src.id" :value="src.id">
{{ src.label }}
</option>
</select>
<label class="inline-label">{{ $t('misc.page') }}</label>
<input v-model.number="editedItem.page_number" type="number" min="0" />
</div>
<div class="edit-row">
<label>{{ $t('misc.system') }}</label>
<select v-model.number="selectedSystemId">
<option v-for="sys in systemOptions" :key="sys.id" :value="sys.id">{{ sys.label }}</option>
</select>
</div>
<div class="edit-actions">
<button class="btn-primary" :disabled="isSaving" @click="saveEdit">
<span v-if="!isSaving">{{ $t('misc.save') }}</span>
<span v-else>{{ $t('misc.saving') }}</span>
</button>
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
{{ $t('misc.cancel') }}
</button>
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<style scoped>
.error-box {
margin: 10px 0;
padding: 10px 12px;
background: #ffe3e3;
color: #8a1c1c;
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>
import API from '../../utils/api'
import {
buildGameSystemParams,
findSystemById,
loadGameSystems as fetchGameSystems,
systemCodeFor as resolveSystemCode,
buildSystemOptions,
} from '../../utils/maintenanceGameSystems'
export default {
name: 'MiscLookupView',
data() {
return {
items: [],
gameSystems: [],
currentGameSystem: null,
sources: [],
editingId: null,
editedItem: null,
selectedSystemId: null,
isLoading: false,
isSaving: false,
error: '',
searchTerm: '',
}
},
async created() {
await this.initialize()
},
computed: {
filteredItems() {
const term = this.searchTerm.trim().toLowerCase()
const list = term
? this.items.filter(it =>
(it.key || '').toLowerCase().includes(term) ||
(it.value || '').toLowerCase().includes(term)
)
: this.items
return [...list].sort((a, b) => (a.key || '').localeCompare(b.key || ''))
},
keyOptions() {
const set = new Set()
this.items.forEach(it => {
if (it.key) set.add(it.key)
})
return Array.from(set.values()).sort()
},
keyOptionsWithCurrent() {
const set = new Set(this.keyOptions)
if (this.editedItem?.key) set.add(String(this.editedItem.key))
return Array.from(set.values()).sort()
},
systemOptions() {
const labelBuilder = system => {
const code = system.code || ''
const name = system.name || ''
if (code && name) return `${code} - ${name}`.trim()
return code || name || String(system.id ?? '')
}
return buildSystemOptions(this.gameSystems, labelBuilder)
},
sourceMap() {
const map = new Map()
this.sources.forEach(src => {
map.set(src.id, src.code || src.name || src.id)
})
return map
},
sourceOptions() {
return this.sources.map(src => ({
id: src.id,
label: src.code ? `${src.code} - ${src.name || ''}`.trim() : src.name || src.id,
}))
},
},
methods: {
async initialize() {
this.error = ''
await this.loadGameSystems()
if (!this.currentGameSystem) return
await this.loadSources()
await this.loadItems()
},
async loadGameSystems() {
try {
const systems = await fetchGameSystems()
this.gameSystems = systems
const active = systems.find(s => s.is_active)
this.currentGameSystem = active || systems[0] || null
} catch (err) {
console.error('Failed to load game systems:', err)
this.error = err.response?.data?.error || err.message
}
},
async loadSources() {
try {
const params = buildGameSystemParams(this.currentGameSystem)
const resp = await API.get('/api/maintenance/gsm-lit-sources', { params })
this.sources = resp.data?.sources || []
} catch (err) {
console.error('Failed to load sources:', err)
this.error = err.response?.data?.error || err.message
}
},
async loadItems() {
this.isLoading = true
this.error = ''
try {
const params = buildGameSystemParams(this.currentGameSystem)
const resp = await API.get('/api/maintenance/gsm-misc', { params })
this.items = resp.data?.misc || []
} catch (err) {
console.error('Failed to load misc:', err)
this.error = err.response?.data?.error || err.message
} finally {
this.isLoading = false
}
},
buildParamsForSystemId(systemId) {
const sys = findSystemById(this.gameSystems, systemId) || this.currentGameSystem
if (!sys) return {}
return buildGameSystemParams(sys)
},
sourceCodeFor(id) {
if (!id) return '-'
const code = this.sourceMap.get(id)
return code || id
},
systemCodeFor(systemId, fallback = '') {
return resolveSystemCode(this.gameSystems, systemId, fallback)
},
startEdit(item) {
this.editingId = item.id
this.editedItem = { ...item }
this.selectedSystemId = item.game_system_id ?? this.currentGameSystem?.id ?? null
},
cancelEdit() {
this.editingId = null
this.editedItem = null
this.selectedSystemId = null
},
async saveEdit() {
if (!this.editedItem) return
const payload = {
key: this.editedItem.key || '',
value: this.editedItem.value || '',
source_id: this.editedItem.source_id || null,
page_number: this.editedItem.page_number || 0,
}
this.isSaving = true
try {
const params = this.buildParamsForSystemId(this.selectedSystemId)
const resp = await API.put(`/api/maintenance/gsm-misc/${this.editingId}`, payload, { params })
const idx = this.items.findIndex(i => i.id === this.editingId)
if (idx !== -1) this.items.splice(idx, 1, resp.data)
this.cancelEdit()
} catch (err) {
console.error('Failed to save misc:', err)
this.error = err.response?.data?.error || err.message
} finally {
this.isSaving = false
}
},
},
}
</script>
@@ -0,0 +1,203 @@
<template>
<div class="header-section">
<h2>{{ $t('maintenance') }} - {{ $t('skillimprovement.title') }}</h2>
</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>
<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('skillimprovement.edit') }}</button></td>
</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" :disabled="isSaving" @click="saveEdit">
<span v-if="!isSaving">{{ $t('skillimprovement.save') }}</span>
<span v-else>{{ $t('skillimprovement.saving') }}</span>
</button>
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
{{ $t('skillimprovement.cancel') }}
</button>
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<style scoped>
.error-box {
margin: 10px 0;
padding: 10px 12px;
background: #ffe3e3;
color: #8a1c1c;
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>
import API from '../../utils/api'
export default {
name: 'SkillImprovementCostView',
data() {
return {
costs: [],
editingId: null,
editedItem: null,
isLoading: false,
isSaving: false,
error: '',
}
},
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 }))
},
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}`)
}
})
return Array.from(seen.entries()).map(([id, label]) => ({ id, label }))
},
},
methods: {
displayCategory(cost) {
return cost.category_name || cost.skillCategoryName || cost.skillCategoryId || cost.category_id
},
displayDifficulty(cost) {
return cost.difficulty_name || cost.skillDifficultyName || cost.skillDifficultyId || cost.difficulty_id
},
async loadCosts() {
this.isLoading = true
this.error = ''
try {
const resp = await API.get('/api/maintenance/skill-improvement-cost2')
this.costs = resp.data?.costs || []
} catch (err) {
console.error('Failed to load costs:', 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,
}
},
cancelEdit() {
this.editingId = null
this.editedItem = 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,
}
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)
this.cancelEdit()
} catch (err) {
console.error('Failed to save cost:', err)
this.error = err.response?.data?.error || err.message
} finally {
this.isSaving = false
}
},
},
}
</script>
@@ -151,6 +151,30 @@
</div>
</div>
<div class="edit-row">
<div class="edit-field">
<label>{{ $t('skill.system') }}</label>
<select v-model.number="selectedSystemId" style="width:140px;">
<option value="">-</option>
<option v-for="system in systemOptions" :key="system.id" :value="system.id">
{{ system.label }}
</option>
</select>
</div>
</div>
<div class="edit-row">
<div class="edit-field">
<label>{{ $t('skill.system') }}</label>
<select v-model.number="createSelectedSystemId" style="width:140px;">
<option value="">-</option>
<option v-for="system in systemOptions" :key="system.id" :value="system.id">
{{ system.label }}
</option>
</select>
</div>
</div>
<div class="edit-row">
<div class="edit-field full-width">
<label>{{ $t('skill.categories') || 'Categories' }}:</label>
@@ -206,7 +230,7 @@
<td>{{ dtaItem.beschreibung || '-' }}</td>
<td>{{ dtaItem.bonuseigenschaft || '-' }}</td>
<td>{{ formatQuelle(dtaItem) }}</td>
<td>{{ dtaItem.game_system || 'midgard' }}</td>
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.game_system || 'midgard') }}</td>
<td>
<button @click="startEdit(index)">Edit</button>
</td>
@@ -336,6 +360,13 @@
<script>
import API from '../../utils/api'
import {
findSystemIdByCode,
getSourceCode,
getSystemCodeById,
loadGameSystems as fetchGameSystems,
buildSystemOptions,
} from '../../utils/maintenanceGameSystems'
export default {
name: "SkillView",
@@ -365,11 +396,17 @@ export default {
filterInnateskill: '',
filterBonuseigenschaft: '',
enhancedSkills: [],
availableSources: []
availableSources: [],
gameSystems: [],
selectedSystemId: null,
createSelectedSystemId: null
}
},
async created() {
await this.loadEnhancedSkills()
await Promise.all([
this.loadGameSystems(),
this.loadEnhancedSkills()
])
},
computed: {
availableCategories() {
@@ -458,9 +495,19 @@ export default {
})
return filtered
},
systemOptions() {
return buildSystemOptions(this.gameSystems)
}
},
methods: {
async loadGameSystems() {
try {
this.gameSystems = await fetchGameSystems()
} catch (error) {
console.error('Failed to load game systems:', error)
}
},
async loadEnhancedSkills() {
try {
const response = await API.get('/api/maintenance/skills-enhanced')
@@ -522,12 +569,18 @@ export default {
})
}
this.selectedSystemId = skill.game_system_id ?? this.findSystemIdByCode(skill.game_system)
this.editingIndex = index
},
getSourceCode(sourceId) {
if (!sourceId || !this.availableSources.length) return ''
const source = this.availableSources.find(s => s.id === sourceId)
return source ? source.code : ''
return getSourceCode(this.availableSources, sourceId)
},
findSystemIdByCode(code) {
return findSystemIdByCode(this.gameSystems, code)
},
getSystemCodeById(systemId, fallback = '') {
return getSystemCodeById(this.gameSystems, systemId, fallback)
},
onCategoryToggle(categoryId) {
// If category was removed, also remove its difficulty setting
@@ -546,6 +599,7 @@ export default {
try {
// Find source ID from code
const source = this.availableSources.find(s => s.code === this.editedItem.sourceCode)
const selectedSystem = this.gameSystems.find(gs => gs.id === this.selectedSystemId)
// Build category_difficulties array
const categoryDifficulties = this.editedItem.selectedCategories.map(catId => ({
@@ -557,7 +611,8 @@ export default {
id: this.editedItem.id,
name: this.editedItem.name,
beschreibung: this.editedItem.beschreibung,
game_system: this.editedItem.game_system || 'midgard',
game_system: selectedSystem ? selectedSystem.code : (this.editedItem.game_system || 'midgard'),
game_system_id: selectedSystem ? selectedSystem.id : (this.editedItem.game_system_id ?? null),
initialwert: this.editedItem.initialwert,
basiswert: this.editedItem.basiswert || 0,
bonuseigenschaft: this.editedItem.bonuseigenschaft,
@@ -581,6 +636,7 @@ export default {
this.editingIndex = -1
this.editedItem = null
this.selectedSystemId = null
} catch (error) {
console.error('Failed to update skill:', error)
alert('Failed to update skill: ' + (error.response?.data?.error || error.message))
@@ -608,10 +664,13 @@ export default {
},
startCreate() {
// Initialize new skill object with defaults
const defaultSystem = this.gameSystems.find(gs => gs.is_active) || this.gameSystems[0] || null
this.createSelectedSystemId = defaultSystem ? defaultSystem.id : null
this.editedItem = {
name: '',
beschreibung: '',
game_system: 'midgard',
game_system: defaultSystem ? defaultSystem.code : 'midgard',
game_system_id: defaultSystem ? defaultSystem.id : null,
initialwert: 5,
basiswert: 0,
bonuseigenschaft: '',
@@ -634,6 +693,7 @@ export default {
// Find source ID from code
const source = this.availableSources.find(s => s.code === this.editedItem.sourceCode)
const selectedSystem = this.gameSystems.find(gs => gs.id === this.createSelectedSystemId)
// Build category_difficulties array
const categoryDifficulties = this.editedItem.selectedCategories.map(catId => ({
@@ -644,7 +704,8 @@ export default {
const createData = {
name: this.editedItem.name,
beschreibung: this.editedItem.beschreibung,
game_system: this.editedItem.game_system || 'midgard',
game_system: selectedSystem ? selectedSystem.code : (this.editedItem.game_system || 'midgard'),
game_system_id: selectedSystem ? selectedSystem.id : (this.editedItem.game_system_id ?? null),
initialwert: this.editedItem.initialwert,
basiswert: this.editedItem.basiswert || 0,
bonuseigenschaft: this.editedItem.bonuseigenschaft,
@@ -666,6 +727,7 @@ export default {
// Hide the create dialog
this.creatingNew = false
this.editedItem = null
this.createSelectedSystemId = null
} catch (error) {
console.error('Failed to create skill:', error)
alert('Failed to create skill: ' + (error.response?.data?.error || error.message))
@@ -675,6 +737,7 @@ export default {
cancelCreate() {
this.creatingNew = false
this.editedItem = null
this.createSelectedSystemId = null
}
}
}
@@ -136,7 +136,7 @@
<td>{{ dtaItem.ursprung || '-' }}</td>
<td>{{ dtaItem.beschreibung || '-' }}</td>
<td>{{ formatQuelle(dtaItem) }}</td>
<td>{{ dtaItem.system || 'midgard' }}</td>
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.system || 'midgard') }}</td>
<td>
<button @click="startEdit(index)">Edit</button>
</td>
@@ -223,7 +223,12 @@
</div>
<div class="edit-field">
<label>{{ $t('spell.system') }}:</label>
<input v-model="editedItem.system" style="width:100px;" />
<select v-model.number="selectedSystemId" style="width:140px;">
<option value="">-</option>
<option v-for="system in systemOptions" :key="system.id" :value="system.id">
{{ system.label }}
</option>
</select>
</div>
</div>
@@ -354,6 +359,13 @@
<script>
import API from '../../utils/api'
import {
findSystemIdByCode,
getSourceCode,
getSystemCodeById,
loadGameSystems as fetchGameSystems,
buildSystemOptions,
} from '../../utils/maintenanceGameSystems'
export default {
name: "SpellView",
props: {
@@ -383,11 +395,16 @@ export default {
filterWirkungsziel: '',
filterQuelle: '',
enhancedSpells: [],
availableSources: []
availableSources: [],
gameSystems: [],
selectedSystemId: null
}
},
async created() {
await this.loadEnhancedSpells()
await Promise.all([
this.loadGameSystems(),
this.loadEnhancedSpells()
])
},
computed: {
availableCategories() {
@@ -502,9 +519,19 @@ export default {
? aValue.localeCompare(bValue)
: bValue.localeCompare(aValue);
});
},
systemOptions() {
return buildSystemOptions(this.gameSystems)
}
},
methods: {
async loadGameSystems() {
try {
this.gameSystems = await fetchGameSystems()
} catch (error) {
console.error('Failed to load game systems:', error)
}
},
async loadEnhancedSpells() {
try {
const response = await API.get('/api/maintenance/spells-enhanced')
@@ -524,17 +551,21 @@ export default {
...spell,
sourceCode: this.getSourceCode(spell.source_id)
}
this.selectedSystemId = spell.game_system_id ?? this.findSystemIdByCode(spell.system)
this.editingIndex = index
},
async saveEdit(index) {
try {
// Find source ID from code
const source = this.availableSources.find(s => s.code === this.editedItem.sourceCode)
const selectedSystem = this.gameSystems.find(gs => gs.id === this.selectedSystemId)
const updateData = {
...this.editedItem,
source_id: source ? source.id : null,
page_number: this.editedItem.page_number || 0
page_number: this.editedItem.page_number || 0,
system: selectedSystem ? selectedSystem.code : (this.editedItem.system || ''),
game_system_id: selectedSystem ? selectedSystem.id : (this.editedItem.game_system_id ?? null)
}
const response = await API.put(
@@ -550,6 +581,7 @@ export default {
this.editingIndex = -1
this.editedItem = null
this.selectedSystemId = null
} catch (error) {
console.error('Failed to save spell:', error)
alert('Failed to save spell: ' + (error.response?.data?.error || error.message))
@@ -558,6 +590,10 @@ export default {
cancelEdit() {
this.editingIndex = -1;
this.editedItem = null;
this.selectedSystemId = null;
},
findSystemIdByCode(code) {
return findSystemIdByCode(this.gameSystems, code)
},
sortBy(field) {
if (this.sortField === field) {
@@ -677,9 +713,10 @@ export default {
return spell.quelle || '-'
},
getSourceCode(sourceId) {
if (!sourceId || !this.availableSources.length) return ''
const source = this.availableSources.find(s => s.id === sourceId)
return source ? source.code : ''
return getSourceCode(this.availableSources, sourceId)
},
getSystemCodeById(systemId, fallback = '') {
return getSystemCodeById(this.gameSystems, systemId, fallback)
},
clearFilters() {
this.searchTerm = ''
@@ -55,7 +55,7 @@
<td>{{ dtaItem.initialwert || '0' }}</td>
<td>{{ dtaItem.beschreibung || '-' }}</td>
<td>{{ formatQuelle(dtaItem) }}</td>
<td>{{ dtaItem.system || 'midgard' }}</td>
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.system || 'midgard') }}</td>
<td>
<button @click="startEdit(index)">Edit</button>
</td>
@@ -109,7 +109,12 @@
</div>
<div class="edit-field">
<label>{{ $t('weaponskill.system') }}:</label>
<input v-model="editedItem.system" style="width:100px;" />
<select v-model.number="selectedSystemId" style="width:140px;">
<option value="">-</option>
<option v-for="system in systemOptions" :key="system.id" :value="system.id">
{{ system.label }}
</option>
</select>
</div>
</div>
@@ -174,6 +179,13 @@
<script>
import API from '../../utils/api'
import {
findSystemIdByCode,
getSourceCode,
getSystemCodeById,
loadGameSystems as fetchGameSystems,
buildSystemOptions,
} from '../../utils/maintenanceGameSystems'
export default {
name: "WaeponSkillView",
props: {
@@ -197,11 +209,16 @@ export default {
filterQuelle: '',
enhancedWeaponSkills: [],
availableSources: [],
availableDifficultiesData: []
availableDifficultiesData: [],
gameSystems: [],
selectedSystemId: null
}
},
async created() {
await this.loadEnhancedWeaponSkills()
await Promise.all([
this.loadGameSystems(),
this.loadEnhancedWeaponSkills()
])
},
computed: {
availableDifficulties() {
@@ -259,9 +276,19 @@ export default {
})
return filtered
},
systemOptions() {
return buildSystemOptions(this.gameSystems)
}
},
methods: {
async loadGameSystems() {
try {
this.gameSystems = await fetchGameSystems()
} catch (error) {
console.error('Failed to load game systems:', error)
}
},
async loadEnhancedWeaponSkills() {
try {
const response = await API.get('/api/maintenance/weaponskills-enhanced')
@@ -278,19 +305,23 @@ export default {
...weaponSkill,
sourceCode: this.getSourceCode(weaponSkill.source_id)
}
this.selectedSystemId = weaponSkill.game_system_id ?? this.findSystemIdByCode(weaponSkill.system)
this.editingIndex = index
},
async saveEdit(index) {
try {
// Find source ID from code
const source = this.availableSources.find(s => s.code === this.editedItem.sourceCode)
const selectedSystem = this.gameSystems.find(gs => gs.id === this.selectedSystemId)
const updateData = {
...this.editedItem,
source_id: source ? source.id : null,
page_number: this.editedItem.page_number || 0,
difficulty: this.editedItem.difficulty,
category: 'Waffen' // Weapon skills always use 'Waffen' category
category: 'Waffen', // Weapon skills always use 'Waffen' category
system: selectedSystem ? selectedSystem.code : (this.editedItem.system || ''),
game_system_id: selectedSystem ? selectedSystem.id : (this.editedItem.game_system_id ?? null)
}
const response = await API.put(
@@ -303,6 +334,7 @@ export default {
this.editingIndex = -1
this.editedItem = null
this.selectedSystemId = null
} catch (error) {
console.error('Failed to save weapon skill:', error)
alert('Failed to save weapon skill: ' + (error.response?.data?.error || error.message))
@@ -311,6 +343,10 @@ export default {
cancelEdit() {
this.editingIndex = -1
this.editedItem = null
this.selectedSystemId = null
},
findSystemIdByCode(code) {
return findSystemIdByCode(this.gameSystems, code)
},
sortBy(field) {
if (this.sortField === field) {
@@ -336,9 +372,10 @@ export default {
return weaponSkill.quelle || '-'
},
getSourceCode(sourceId) {
if (!sourceId || !this.availableSources.length) return ''
const source = this.availableSources.find(s => s.id === sourceId)
return source ? source.code : ''
return getSourceCode(this.availableSources, sourceId)
},
getSystemCodeById(systemId, fallback = '') {
return getSystemCodeById(this.gameSystems, systemId, fallback)
},
clearFilters() {
this.searchTerm = ''
@@ -96,7 +96,7 @@
<td>{{ dtaItem.beschreibung || '-' }}</td>
<td>{{ formatQuelle(dtaItem) }}</td>
<td><input type="checkbox" :checked="dtaItem.personal_item" disabled /></td>
<td>{{ dtaItem.system || 'midgard' }}</td>
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.system || 'midgard') }}</td>
<td>
<button @click="startEdit(index)">Edit</button>
</td>
@@ -191,7 +191,12 @@
</div>
<div class="edit-field">
<label>{{ $t('weapon.system') }}:</label>
<input v-model="editedItem.system" style="width:100px;" />
<select v-model.number="selectedSystemId" style="width:140px;">
<option value="">-</option>
<option v-for="system in systemOptions" :key="system.id" :value="system.id">
{{ system.label }}
</option>
</select>
</div>
</div>
@@ -256,6 +261,13 @@
<script>
import API from '../../utils/api'
import {
findSystemIdByCode,
getSourceCode,
getSystemCodeById,
loadGameSystems as fetchGameSystems,
buildSystemOptions,
} from '../../utils/maintenanceGameSystems'
export default {
name: "WeaponView",
props: {
@@ -282,11 +294,16 @@ export default {
filterRangeFar: '',
filterQuelle: '',
enhancedWeapons: [],
availableSources: []
availableSources: [],
gameSystems: [],
selectedSystemId: null
}
},
async created() {
await this.loadEnhancedWeapons()
await Promise.all([
this.loadGameSystems(),
this.loadEnhancedWeapons()
])
},
computed: {
availableSkillsRequired() {
@@ -392,9 +409,19 @@ export default {
})
return filtered
},
systemOptions() {
return buildSystemOptions(this.gameSystems)
}
},
methods: {
async loadGameSystems() {
try {
this.gameSystems = await fetchGameSystems()
} catch (error) {
console.error('Failed to load game systems:', error)
}
},
async loadEnhancedWeapons() {
try {
const response = await API.get('/api/maintenance/weapons-enhanced')
@@ -410,17 +437,21 @@ export default {
...weapon,
sourceCode: this.getSourceCode(weapon.source_id)
}
this.selectedSystemId = weapon.game_system_id ?? this.findSystemIdByCode(weapon.system)
this.editingIndex = index
},
async saveEdit(index) {
try {
// Find source ID from code
const source = this.availableSources.find(s => s.code === this.editedItem.sourceCode)
const selectedSystem = this.gameSystems.find(gs => gs.id === this.selectedSystemId)
const updateData = {
...this.editedItem,
source_id: source ? source.id : null,
page_number: this.editedItem.page_number || 0
page_number: this.editedItem.page_number || 0,
system: selectedSystem ? selectedSystem.code : (this.editedItem.system || ''),
game_system_id: selectedSystem ? selectedSystem.id : (this.editedItem.game_system_id ?? null)
}
const response = await API.put(
@@ -436,6 +467,7 @@ export default {
this.editingIndex = -1
this.editedItem = null
this.selectedSystemId = null
} catch (error) {
console.error('Failed to save weapon:', error)
alert('Failed to save weapon: ' + (error.response?.data?.error || error.message))
@@ -444,6 +476,10 @@ export default {
cancelEdit() {
this.editingIndex = -1
this.editedItem = null
this.selectedSystemId = null
},
findSystemIdByCode(code) {
return findSystemIdByCode(this.gameSystems, code)
},
sortBy(field) {
if (this.sortField === field) {
@@ -469,9 +505,10 @@ export default {
return weapon.quelle || '-'
},
getSourceCode(sourceId) {
if (!sourceId || !this.availableSources.length) return ''
const source = this.availableSources.find(s => s.id === sourceId)
return source ? source.code : ''
return getSourceCode(this.availableSources, sourceId)
},
getSystemCodeById(systemId, fallback = '') {
return getSystemCodeById(this.gameSystems, systemId, fallback)
},
clearFilters() {
this.searchTerm = ''
+90 -1
View File
@@ -279,6 +279,80 @@ export default {
spell:'Zauber',
equipment:'Ausrüstung',
weapon:'Waffen',
believe:'Glaubensrichtungen',
gamesystem:'Spielsysteme',
litsource:'Literaturquellen',
misc:'Sonstige',
skillimprovement:'Steigerungskosten',
},
believe: {
title: 'Glaubensrichtungen',
id: 'ID',
name: 'Name',
nameRequired: 'Name darf nicht leer sein.',
description: 'Beschreibung',
source: 'Quelle',
page: 'Seite',
system: 'System',
edit: 'Bearbeiten',
save: 'Speichern',
saving: 'Speichern...',
cancel: 'Abbrechen',
sourceNone: 'Keine Quelle'
},
gamesystem: {
title: 'Spielsysteme',
id: 'ID',
code: 'Code',
name: 'Name',
description: 'Beschreibung',
active: 'Aktiv',
edit: 'Bearbeiten',
save: 'Speichern',
saving: 'Speichern...',
cancel: 'Abbrechen'
},
litsource: {
title: 'Literaturquellen',
id: 'ID',
code: 'Code',
name: 'Name',
fullName: 'Vollständiger Name',
edition: 'Edition',
publisher: 'Verlag',
year: 'Jahr',
description: 'Beschreibung',
active: 'Aktiv',
core: 'Kernquelle',
edit: 'Bearbeiten',
save: 'Speichern',
saving: 'Speichern...',
cancel: 'Abbrechen'
},
misc: {
title: 'Sonstige Einträge',
id: 'ID',
key: 'Schlüssel',
value: 'Wert',
source: 'Quelle',
page: 'Seite',
system: 'System',
edit: 'Bearbeiten',
save: 'Speichern',
saving: 'Speichern...',
cancel: 'Abbrechen'
},
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'
},
search:'Suche',
Skill:'Fertigkeit',
@@ -360,6 +434,8 @@ export default {
unfree: 'Unfrei',
religion: 'Religion/Glaube',
religionPlaceholder: 'Mindestens 2 Zeichen für die Suche eingeben...',
religionHelp: 'Gib mindestens zwei Buchstaben ein, um passende Glaubensrichtungen zu finden. Klicken zum Auswählen. Kann später geändert und erweitert werden',
characterNameHelp: 'Geben Sie den Namen Ihres Charakters ein. Dieser Name wird in der Charakterübersicht und im Datenblatt angezeigt. Kann zur Zeit nich geändert werden',
selected: 'Ausgewählt',
nextAttributes: 'Weiter: Attribute →',
required: '*',
@@ -543,6 +619,7 @@ export default {
loading: 'Lade Benutzer...',
loadError: 'Fehler beim Laden der Benutzer',
id: 'ID',
displayName: 'Anzeigename',
username: 'Benutzername',
email: 'E-Mail',
role: 'Rolle',
@@ -581,10 +658,15 @@ export default {
title: 'Benutzerprofil',
loading: 'Lade Profil...',
userInfo: 'Benutzerinformationen',
displayName: 'Anzeigename',
username: 'Benutzername',
currentEmail: 'Aktuelle E-Mail',
role: 'Rolle',
language: 'Sprache',
changeDisplayName: 'Anzeigenamen ändern',
displayNamePlaceholder: 'Maximal 30 Zeichen',
displayNameHelper: 'Alle Zeichen sind erlaubt. Maximale Länge: 30.',
updateDisplayName: 'Anzeigenamen aktualisieren',
changeLanguage: 'Sprache ändern',
selectLanguage: 'Sprache auswählen',
updateLanguage: 'Sprache aktualisieren',
@@ -614,7 +696,10 @@ export default {
passwordMismatch: 'Die Passwörter stimmen nicht überein',
passwordUpdateSuccess: 'Passwort erfolgreich aktualisiert',
passwordUpdateError: 'Fehler beim Aktualisieren des Passworts',
currentPasswordIncorrect: 'Das aktuelle Passwort ist falsch'
currentPasswordIncorrect: 'Das aktuelle Passwort ist falsch',
displayNameUpdateSuccess: 'Anzeigename erfolgreich aktualisiert',
displayNameUpdateError: 'Fehler beim Aktualisieren des Anzeigenamens',
displayNameTooLong: 'Anzeigename darf maximal 30 Zeichen lang sein'
},
character: {
uploadImage: 'Bild hochladen',
@@ -704,6 +789,10 @@ export default {
frontend: 'Frontend',
backend: 'Backend',
version: 'Version',
system: 'System',
userCount: 'Benutzeranzahl',
charCount: 'Charakteranzahl',
dbVersion: 'Datenbankversion',
commit: 'Commit',
status: 'Status',
statusAvailable: 'Verfügbar',
+90 -1
View File
@@ -275,6 +275,80 @@ export default {
spell:'Zauber',
equipment:'Ausrüstung',
weapon:'Waffen',
believe:'Beliefs',
gamesystem:'Game Systems',
litsource:'Sources',
misc:'Misc',
skillimprovement:'Improvement Costs',
},
believe: {
title: 'Beliefs',
id: 'ID',
name: 'Name',
nameRequired: 'Name is required.',
description: 'Description',
source: 'Source',
page: 'Page',
system: 'System',
edit: 'Edit',
save: 'Save',
saving: 'Saving...',
cancel: 'Cancel',
sourceNone: 'No source'
},
gamesystem: {
title: 'Game Systems',
id: 'ID',
code: 'Code',
name: 'Name',
description: 'Description',
active: 'Active',
edit: 'Edit',
save: 'Save',
saving: 'Saving...',
cancel: 'Cancel'
},
litsource: {
title: 'Literature Sources',
id: 'ID',
code: 'Code',
name: 'Name',
fullName: 'Full name',
edition: 'Edition',
publisher: 'Publisher',
year: 'Year',
description: 'Description',
active: 'Active',
core: 'Core source',
edit: 'Edit',
save: 'Save',
saving: 'Saving...',
cancel: 'Cancel'
},
misc: {
title: 'Misc Entries',
id: 'ID',
key: 'Key',
value: 'Value',
source: 'Source',
page: 'Page',
system: 'System',
edit: 'Edit',
save: 'Save',
saving: 'Saving...',
cancel: 'Cancel'
},
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'
},
search:'Suche',
Skill:'Fertigkeit',
@@ -356,6 +430,8 @@ export default {
unfree: 'Unfree',
religion: 'Religion/Belief',
religionPlaceholder: 'Type at least 2 characters to search beliefs...',
religionHelp: 'Type two or more letters to search available beliefs, then click a result to select it. Later you can change and extend it',
characterNameHelp: 'Enter the name of your character. This name will be displayed in the character overview and datasheet. Cannot be changed at the moment',
selected: 'Selected',
nextAttributes: 'Next: Attributes →',
required: '*',
@@ -539,6 +615,7 @@ export default {
loading: 'Loading users...',
loadError: 'Failed to load users',
id: 'ID',
displayName: 'Display Name',
username: 'Username',
email: 'Email',
role: 'Role',
@@ -577,10 +654,15 @@ export default {
title: 'User Profile',
loading: 'Loading profile...',
userInfo: 'User Information',
displayName: 'Display Name',
username: 'Username',
currentEmail: 'Current Email',
role: 'Role',
language: 'Language',
changeDisplayName: 'Change Display Name',
displayNamePlaceholder: 'Up to 30 characters',
displayNameHelper: 'Any characters are allowed. Maximum length: 30.',
updateDisplayName: 'Update Display Name',
changeLanguage: 'Change Language',
selectLanguage: 'Select Language',
updateLanguage: 'Update Language',
@@ -610,7 +692,10 @@ export default {
passwordMismatch: 'Passwords do not match',
passwordUpdateSuccess: 'Password updated successfully',
passwordUpdateError: 'Failed to update password',
currentPasswordIncorrect: 'Current password is incorrect'
currentPasswordIncorrect: 'Current password is incorrect',
displayNameUpdateSuccess: 'Display name updated successfully',
displayNameUpdateError: 'Failed to update display name',
displayNameTooLong: 'Display name must be at most 30 characters'
},
character: {
uploadImage: 'Upload Image',
@@ -699,6 +784,10 @@ export default {
frontend: 'Frontend',
backend: 'Backend',
version: 'Version',
system: 'System',
userCount: 'User Count',
charCount: 'Character Count',
dbVersion: 'Database Version',
commit: 'Commit',
status: 'Status',
statusAvailable: 'Available',
+6 -4
View File
@@ -21,12 +21,14 @@ export const useUserStore = defineStore('user', {
this.isLoading = true
try {
const response = await API.get('/api/user/profile')
this.currentUser = response.data
const profile = { ...response.data }
profile.display_name = profile.display_name || profile.username
this.currentUser = profile
// Set user's preferred language
if (response.data.preferred_language) {
i18n.global.locale.value = response.data.preferred_language
localStorage.setItem('language', response.data.preferred_language)
if (profile.preferred_language) {
i18n.global.locale.value = profile.preferred_language
localStorage.setItem('language', profile.preferred_language)
}
} catch (error) {
console.error('Failed to fetch user profile:', error)
@@ -0,0 +1,66 @@
import API from './api'
const defaultSystemLabel = (system = {}) => {
const code = system.code || ''
const name = system.name || ''
if (code && name) return `${code} (${name})`
return code || name || String(system.id ?? '')
}
export const normalizeSystem = (gs = {}) => ({
...gs,
id: gs.id ?? gs.ID ?? gs.Id ?? null,
code: gs.code ?? gs.Code ?? '',
name: gs.name ?? gs.Name ?? '',
description: gs.description ?? gs.Description ?? '',
is_active: gs.is_active ?? gs.IsActive ?? gs.isActive ?? false,
})
export const buildSystemOptions = (gameSystems = [], labelBuilder = defaultSystemLabel) =>
gameSystems.map(system => ({
id: system.id,
label: labelBuilder(system),
}))
// Alias retained for existing component imports
export const systemOptionsFor = buildSystemOptions
export const findSystemById = (gameSystems = [], id) => {
if (id === null || id === undefined) return null
return gameSystems.find(gs => gs.id === id) || null
}
export const findSystemIdByCode = (gameSystems = [], code) => {
if (!code) return null
const match = gameSystems.find(gs => gs.code === code)
return match ? match.id : null
}
export const buildGameSystemParams = system => {
if (!system) return {}
return {
game_system_id: system.id,
game_system: system.name,
}
}
export const getSystemCodeById = (gameSystems = [], systemId, fallback = '') => {
if (!systemId) return fallback
const sys = gameSystems.find(gs => gs.id === systemId)
return sys ? sys.code || fallback : fallback
}
// Alias retained for existing component imports
export const systemCodeFor = getSystemCodeById
export const getSourceCode = (sources = [], sourceId) => {
if (!sourceId) return ''
const source = sources.find(src => src.id === sourceId)
return source ? source.code || '' : ''
}
export const loadGameSystems = async () => {
const resp = await API.get('/api/maintenance/game-systems')
const systems = resp.data?.game_systems || []
return systems.map(normalizeSystem)
}
+1 -1
View File
@@ -1,5 +1,5 @@
// Frontend version information
export const VERSION = '0.2.1'
export const VERSION = '0.2.2'
// Git commit will be injected at build time or detected from env
export const GIT_COMMIT = import.meta.env.VITE_GIT_COMMIT || 'unknown'
+19 -3
View File
@@ -19,17 +19,24 @@
<div class="card">
<h4>{{ $t('systemInfo.frontend') }}</h4>
<p><strong>{{ $t('systemInfo.version') }}:</strong> {{ frontendVersion }}</p>
<p><strong>{{ $t('systemInfo.commit') }}:</strong> <code>{{ frontendCommit }}</code></p>
<!--<p><strong>{{ $t('systemInfo.commit') }}:</strong> <code>{{ frontendCommit }}</code></p>-->
</div>
<div class="card">
<h4>{{ $t('systemInfo.backend') }}</h4>
<p><strong>{{ $t('systemInfo.version') }}:</strong> {{ backendVersion }}</p>
<p><strong>{{ $t('systemInfo.commit') }}:</strong> <code>{{ backendCommit }}</code></p>
<!--<p><strong>{{ $t('systemInfo.commit') }}:</strong> <code>{{ backendCommit }}</code></p>-->
<p><strong>{{ $t('systemInfo.status') }}:</strong>
<span :class="statusClass">{{ statusText }}</span>
</p>
</div>
<div class="card">
<h4>{{ $t('systemInfo.system') }}</h4>
<p><strong>{{ $t('systemInfo.userCount') }}:</strong> {{ userCount }}</p>
<p><strong>{{ $t('systemInfo.charCount') }}:</strong> {{ charCount }}</p>
<p><strong>{{ $t('systemInfo.dbVersion') }}:</strong> {{ dbVersion ||"N/A" }}</p>
</div>
</div>
<div class="section-header">
@@ -119,6 +126,9 @@ export default {
frontendCommit: getGitCommit(),
backendVersion: "Loading...",
backendCommit: "Loading...",
userCount: "Loading...",
charCount: "Loading...",
dbVersion: "Loading...",
githubUrl: "https://github.com/Bardioc26/bamort",
koFiUrl: "https://ko-fi.com/bardioc26",
}
@@ -148,16 +158,22 @@ export default {
async fetchBackendVersion() {
try {
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8180'
const response = await axios.get(`${apiUrl}/api/public/version`)
const response = await axios.get(`${apiUrl}/api/public/systeminfo`)
if (response.data) {
this.backendVersion = response.data.version || "Unknown"
this.backendCommit = response.data.gitCommit || "Unknown"
this.userCount = response.data.userCount || "Unknown"
this.charCount = response.data.charCount || "Unknown"
this.dbVersion = response.data.dbVersion || "Unknown"
}
} catch (error) {
console.warn("Could not fetch backend version:", error)
this.backendVersion = "Unavailable"
this.backendCommit = "N/A"
this.userCount = "N/A"
this.charCount = "N/A"
this.dbVersion = "N/A"
}
}
}
+5 -3
View File
@@ -14,6 +14,7 @@
<tr>
<th>{{ $t('userManagement.id') }}</th>
<th>{{ $t('userManagement.username') }}</th>
<th>{{ $t('userManagement.displayName') }}</th>
<th>{{ $t('userManagement.email') }}</th>
<th>{{ $t('userManagement.role') }}</th>
<th>{{ $t('userManagement.createdAt') }}</th>
@@ -24,6 +25,7 @@
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.display_name }}</td>
<td>{{ user.email }}</td>
<td>
<span :class="getRoleBadgeClass(user.role)">
@@ -65,7 +67,7 @@
<h3>{{ $t('userManagement.changeRoleTitle') }}</h3>
</div>
<div class="modal-body">
<p>{{ $t('userManagement.changeRoleFor') }}: <strong>{{ selectedUser.username }}</strong></p>
<p>{{ $t('userManagement.changeRoleFor') }}: <strong>{{ selectedUser.display_name || selectedUser.username }}</strong></p>
<div class="form-group">
<label>{{ $t('userManagement.selectRole') }}</label>
<select v-model="newRole" class="form-control">
@@ -93,7 +95,7 @@
<h3>{{ $t('userManagement.deleteUserTitle') }}</h3>
</div>
<div class="modal-body">
<p>{{ $t('userManagement.deleteConfirm') }}: <strong>{{ selectedUser.username }}</strong>?</p>
<p>{{ $t('userManagement.deleteConfirm') }}: <strong>{{ selectedUser.display_name || selectedUser.username }}</strong>?</p>
<p class="badge badge-warning">{{ $t('userManagement.deleteWarning') }}</p>
</div>
<div class="modal-footer">
@@ -114,7 +116,7 @@
<h3>{{ $t('userManagement.changePasswordTitle') }}</h3>
</div>
<div class="modal-body">
<p>{{ $t('userManagement.changePasswordFor') }}: <strong>{{ selectedUser.username }}</strong></p>
<p>{{ $t('userManagement.changePasswordFor') }}: <strong>{{ selectedUser.display_name || selectedUser.username }}</strong></p>
<div class="form-group">
<label>{{ $t('userManagement.newPassword') }}</label>
<input
+65 -1
View File
@@ -11,6 +11,10 @@
<!-- User Information Section -->
<div class="profile-section">
<h2>{{ $t('profile.userInfo') }}</h2>
<div class="info-row">
<label>{{ $t('profile.displayName') }}:</label>
<span>{{ userProfile.display_name || userProfile.username }}</span>
</div>
<div class="info-row">
<label>{{ $t('profile.username') }}:</label>
<span>{{ userProfile.username }}</span>
@@ -31,6 +35,28 @@
</div>
</div>
<!-- Change Display Name Section -->
<div class="profile-section">
<h2>{{ $t('profile.changeDisplayName') }}</h2>
<form @submit.prevent="updateDisplayName" class="profile-form">
<div class="form-group">
<label for="displayName">{{ $t('profile.displayName') }}:</label>
<input
type="text"
id="displayName"
v-model="displayNameForm.newDisplayName"
:placeholder="$t('profile.displayNamePlaceholder')"
maxlength="30"
/>
<small>{{ $t('profile.displayNameHelper') }}</small>
</div>
<button type="submit" :disabled="isUpdating" class="btn-primary">
<span v-if="!isUpdating">{{ $t('profile.updateDisplayName') }}</span>
<span v-else>{{ $t('profile.updating') }}</span>
</button>
</form>
</div>
<!-- Change Language Section -->
<div class="profile-section">
<h2>{{ $t('profile.changeLanguage') }}</h2>
@@ -222,6 +248,7 @@ h1 {
<script>
import API from '../utils/api'
import { useUserStore } from '../stores/userStore'
export default {
name: 'UserProfileView',
@@ -231,10 +258,14 @@ export default {
isUpdating: false,
userProfile: {
username: '',
display_name: '',
email: '',
role: 'standard',
preferred_language: 'de'
},
displayNameForm: {
newDisplayName: ''
},
languageForm: {
selectedLanguage: 'de'
},
@@ -259,6 +290,7 @@ export default {
this.userProfile = response.data
this.emailForm.newEmail = this.userProfile.email
this.languageForm.selectedLanguage = this.userProfile.preferred_language || 'de'
this.displayNameForm.newDisplayName = this.userProfile.display_name || ''
} catch (error) {
console.error('Failed to load profile:', error)
alert(this.$t('profile.loadError') + ': ' + (error.response?.data?.error || error.message))
@@ -269,6 +301,38 @@ export default {
getRoleBadgeClass(role) {
return `badge-role-${role}`
},
async updateDisplayName() {
if (this.displayNameForm.newDisplayName.length > 30) {
alert(this.$t('profile.displayNameTooLong'))
return
}
this.isUpdating = true
try {
const response = await API.put('/api/user/display-name', {
display_name: this.displayNameForm.newDisplayName
})
this.userProfile.display_name = response.data.display_name || this.userProfile.username
this.displayNameForm.newDisplayName = this.userProfile.display_name
const userStore = useUserStore()
if (userStore.currentUser) {
userStore.currentUser.display_name = this.userProfile.display_name
}
//alert(this.$t('profile.displayNameUpdateSuccess'))
} catch (error) {
console.error('Failed to update display name:', error)
let errorMsg = this.$t('profile.displayNameUpdateError')
if (error.response?.data?.error) {
errorMsg = error.response.data.error
}
alert(errorMsg)
} finally {
this.isUpdating = false
}
},
async updateEmail() {
if (!this.emailForm.newEmail) {
alert(this.$t('profile.emailRequired'))
@@ -287,7 +351,7 @@ export default {
})
this.userProfile.email = response.data.email
alert(this.$t('profile.emailUpdateSuccess'))
//alert(this.$t('profile.emailUpdateSuccess'))
} catch (error) {
console.error('Failed to update email:', error)
let errorMsg = this.$t('profile.emailUpdateError')