bugfix-0.2.4
Bugfix 0.2.4 * edit in Skill view * 404 when learning or improving skills and spells * password restet e-mail was not sent
This commit is contained in:
@@ -5,7 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Version is the application version
|
// Version is the application version
|
||||||
const Version = "0.2.2"
|
const Version = "0.2.4"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// GitCommit will be set by build flags or detected at runtime
|
// GitCommit will be set by build flags or detected at runtime
|
||||||
|
|||||||
@@ -191,6 +191,12 @@ func ToFeChar(object *models.Char) *models.FeChar {
|
|||||||
feC := &models.FeChar{
|
feC := &models.FeChar{
|
||||||
Char: *object,
|
Char: *object,
|
||||||
}
|
}
|
||||||
|
for idx, fertigkeit := range object.Fertigkeiten {
|
||||||
|
fertigkeit.Bonus = GetSkillBonus(&object.Eigenschaften, &fertigkeit)
|
||||||
|
if fertigkeit.Bonus > 0 {
|
||||||
|
object.Fertigkeiten[idx].Bonus = fertigkeit.Bonus
|
||||||
|
}
|
||||||
|
}
|
||||||
skills, innateSkills, categories := splitSkills(object.Fertigkeiten)
|
skills, innateSkills, categories := splitSkills(object.Fertigkeiten)
|
||||||
feC.Fertigkeiten = skills
|
feC.Fertigkeiten = skills
|
||||||
feC.InnateSkills = innateSkills
|
feC.InnateSkills = innateSkills
|
||||||
@@ -199,6 +205,32 @@ func ToFeChar(object *models.Char) *models.FeChar {
|
|||||||
return feC
|
return feC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetSkillBonus(eigenschaften *[]models.Eigenschaft, skill *models.SkFertigkeit) int {
|
||||||
|
bonus := 0
|
||||||
|
gsmsk := skill.GetSkillByName()
|
||||||
|
if gsmsk.Bonuseigenschaft != "check" {
|
||||||
|
for _, eigenschaft := range *eigenschaften {
|
||||||
|
if eigenschaft.Name == gsmsk.Bonuseigenschaft {
|
||||||
|
if eigenschaft.Value < 6 {
|
||||||
|
bonus = -2
|
||||||
|
break
|
||||||
|
} else if eigenschaft.Value < 21 {
|
||||||
|
bonus = -1
|
||||||
|
break
|
||||||
|
} else if eigenschaft.Value > 81 && eigenschaft.Value < 96 {
|
||||||
|
bonus = 1
|
||||||
|
break
|
||||||
|
} else if eigenschaft.Value >= 96 {
|
||||||
|
bonus = 2
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
skill.Bonus = bonus
|
||||||
|
return bonus
|
||||||
|
}
|
||||||
|
|
||||||
func splitSkills(object []models.SkFertigkeit) ([]models.SkFertigkeit, []models.SkFertigkeit, map[string][]models.SkFertigkeit) {
|
func splitSkills(object []models.SkFertigkeit) ([]models.SkFertigkeit, []models.SkFertigkeit, map[string][]models.SkFertigkeit) {
|
||||||
var normSkills []models.SkFertigkeit
|
var normSkills []models.SkFertigkeit
|
||||||
var innateSkills []models.SkFertigkeit
|
var innateSkills []models.SkFertigkeit
|
||||||
|
|||||||
@@ -1198,3 +1198,15 @@ func TestSearchBeliefs(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToFeChar(t *testing.T) {
|
||||||
|
// Setup test database
|
||||||
|
database.SetupTestDB(true)
|
||||||
|
defer database.ResetTestDB()
|
||||||
|
char := &models.Char{}
|
||||||
|
char.FirstID("18")
|
||||||
|
feChar := ToFeChar(char)
|
||||||
|
assert.Equal(t, "18", feChar.ID)
|
||||||
|
assert.Equal(t, 2, feChar.Fertigkeiten[6].Bonus)
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,20 +33,24 @@ func RegisterRoutes(r *gin.RouterGroup) {
|
|||||||
// im Frontend wir nur noch der neue Endpunkt benutzt
|
// im Frontend wir nur noch der neue Endpunkt benutzt
|
||||||
//charGrp.POST("/lerncost", GetLernCost) // alter Hauptendpunkt für alle Kostenberechnungen (verwendet lerningCostsData)
|
//charGrp.POST("/lerncost", GetLernCost) // alter Hauptendpunkt für alle Kostenberechnungen (verwendet lerningCostsData)
|
||||||
charGrp.POST("/lerncost-new", GetLernCostNewSystem) // neuer Hauptendpunkt für alle Kostenberechnungen (verwendet neue Datenbank)
|
charGrp.POST("/lerncost-new", GetLernCostNewSystem) // neuer Hauptendpunkt für alle Kostenberechnungen (verwendet neue Datenbank)
|
||||||
|
charGrp.POST("/lerncost", GetLernCostNewSystem) // neuer Hauptendpunkt für alle Kostenberechnungen (verwendet neue Datenbank)
|
||||||
charGrp.POST("/improve-skill-new", ImproveSkill) // Fertigkeit verbessern
|
charGrp.POST("/improve-skill-new", ImproveSkill) // Fertigkeit verbessern
|
||||||
|
charGrp.POST("/improve-skill", ImproveSkill) // Fertigkeit verbessern
|
||||||
|
|
||||||
// Lernen und Verbessern (mit automatischem Audit-Log)
|
// Lernen und Verbessern (mit automatischem Audit-Log)
|
||||||
charGrp.POST("/:id/learn-skill-new", LearnSkill) // Fertigkeit lernen (neues System)
|
charGrp.POST("/:id/learn-skill-new", LearnSkill) // Fertigkeit lernen (neues System)
|
||||||
//charGrp.POST("/:id/learn-skill", LearnSkillOld) // Fertigkeit lernen (altes System)
|
charGrp.POST("/:id/learn-skill", LearnSkill) // Fertigkeit lernen (altes System)
|
||||||
charGrp.POST("/:id/learn-spell-new", LearnSpell) // Zauber lernen (neues System)
|
charGrp.POST("/:id/learn-spell-new", LearnSpell) // Zauber lernen (neues System)
|
||||||
//charGrp.POST("/:id/learn-spell", LearnSpellOld) // Zauber lernen (altes System)
|
charGrp.POST("/:id/learn-spell", LearnSpell) // Zauber lernen (altes System)
|
||||||
|
|
||||||
// Fertigkeiten-Information
|
// Fertigkeiten-Information
|
||||||
//charGrp.GET("/:id/available-skills", GetAvailableSkillsOld) // Verfügbare Fertigkeiten mit Kosten (bereits gelernte ausgeschlossen)
|
//charGrp.GET("/:id/available-skills", GetAvailableSkillsOld) // Verfügbare Fertigkeiten mit Kosten (bereits gelernte ausgeschlossen)
|
||||||
charGrp.POST("/available-skills-new", GetAvailableSkillsNewSystem) // Verfügbare Fertigkeiten mit Kosten (bereits gelernte ausgeschlossen)
|
charGrp.POST("/available-skills-new", GetAvailableSkillsNewSystem) // Verfügbare Fertigkeiten mit Kosten (bereits gelernte ausgeschlossen)
|
||||||
|
charGrp.POST("/available-skills", GetAvailableSkillsNewSystem) // Verfügbare Fertigkeiten mit Kosten (bereits gelernte ausgeschlossen)
|
||||||
charGrp.POST("/available-skills-creation", GetAvailableSkillsForCreation) // Verfügbare Fertigkeiten mit Lernkosten für Charaktererstellung
|
charGrp.POST("/available-skills-creation", GetAvailableSkillsForCreation) // Verfügbare Fertigkeiten mit Lernkosten für Charaktererstellung
|
||||||
charGrp.POST("/available-spells-creation", GetAvailableSpellsForCreation) // Verfügbare Zauber mit Lernkosten für Charaktererstellung
|
charGrp.POST("/available-spells-creation", GetAvailableSpellsForCreation) // Verfügbare Zauber mit Lernkosten für Charaktererstellung
|
||||||
charGrp.POST("/available-spells-new", GetAvailableSpellsNewSystem) // Verfügbare Zauber mit Kosten (bereits gelernte ausgeschlossen)
|
charGrp.POST("/available-spells-new", GetAvailableSpellsNewSystem) // Verfügbare Zauber mit Kosten (bereits gelernte ausgeschlossen)
|
||||||
|
charGrp.POST("/available-spells", GetAvailableSpellsNewSystem) // Verfügbare Zauber mit Kosten (bereits gelernte ausgeschlossen)
|
||||||
charGrp.GET("/spell-details", GetSpellDetails) // Detaillierte Informationen zu einem bestimmten Zauber
|
charGrp.GET("/spell-details", GetSpellDetails) // Detaillierte Informationen zu einem bestimmten Zauber
|
||||||
|
|
||||||
// Belohnungsarten für verschiedene Lernszenarien
|
// Belohnungsarten für verschiedene Lernszenarien
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ type Config struct {
|
|||||||
// PDF Templates
|
// PDF Templates
|
||||||
TemplatesDir string // Directory where PDF templates are stored
|
TemplatesDir string // Directory where PDF templates are stored
|
||||||
ExportTempDir string // Directory for temporary PDF exports
|
ExportTempDir string // Directory for temporary PDF exports
|
||||||
|
|
||||||
|
// Mail Configuration
|
||||||
|
MailHost string // SMTP server host
|
||||||
|
MailPort int // SMTP server port
|
||||||
|
MailUsername string // SMTP username
|
||||||
|
MailPassword string // SMTP password
|
||||||
|
MailFrom string // Default sender email address
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cfg ist die globale Konfigurationsvariable
|
// Cfg ist die globale Konfigurationsvariable
|
||||||
@@ -56,6 +63,11 @@ func defaultConfig() *Config {
|
|||||||
FrontendURL: "http://localhost:5173", // Default frontend URL for development
|
FrontendURL: "http://localhost:5173", // Default frontend URL for development
|
||||||
TemplatesDir: "./templates", // Default templates directory
|
TemplatesDir: "./templates", // Default templates directory
|
||||||
ExportTempDir: "./xporttemp", // Default export temp directory
|
ExportTempDir: "./xporttemp", // Default export temp directory
|
||||||
|
MailHost: "", // No default, must be configured
|
||||||
|
MailPort: 465, // Default SMTP SSL port
|
||||||
|
MailUsername: "", // No default, must be configured
|
||||||
|
MailPassword: "", // No default, must be configured
|
||||||
|
MailFrom: "", // No default, must be configured
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +148,28 @@ func LoadConfig() *Config {
|
|||||||
config.ExportTempDir = exportTempDir
|
config.ExportTempDir = exportTempDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mail Configuration
|
||||||
|
if mailHost := os.Getenv("MAIL_HOST"); mailHost != "" {
|
||||||
|
config.MailHost = mailHost
|
||||||
|
}
|
||||||
|
if mailPort := os.Getenv("MAIL_PORT"); mailPort != "" {
|
||||||
|
if port, err := strconv.Atoi(mailPort); err == nil {
|
||||||
|
config.MailPort = port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mailUsername := os.Getenv("MAIL_USERNAME"); mailUsername != "" {
|
||||||
|
config.MailUsername = mailUsername
|
||||||
|
}
|
||||||
|
if mailPassword := os.Getenv("MAIL_PASSWORD"); mailPassword != "" {
|
||||||
|
config.MailPassword = mailPassword
|
||||||
|
}
|
||||||
|
if mailFrom := os.Getenv("MAIL_FROM"); mailFrom != "" {
|
||||||
|
config.MailFrom = mailFrom
|
||||||
|
} else if config.MailUsername != "" {
|
||||||
|
// Fallback: Verwende Username als From-Adresse
|
||||||
|
config.MailFrom = config.MailUsername
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("DEBUG LoadConfig - Finale Config: Environment='%s', DevTesting='%s', DatabaseType='%s'\n Complete: %v\n",
|
fmt.Printf("DEBUG LoadConfig - Finale Config: Environment='%s', DevTesting='%s', DatabaseType='%s'\n Complete: %v\n",
|
||||||
config.Environment, config.DevTesting, config.DatabaseType, config)
|
config.Environment, config.DevTesting, config.DatabaseType, config)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net/smtp"
|
||||||
|
|
||||||
|
"bamort/config"
|
||||||
|
"bamort/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client represents an SMTP mail client
|
||||||
|
type Client struct {
|
||||||
|
host string
|
||||||
|
port int
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
from string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message represents an email message
|
||||||
|
type Message struct {
|
||||||
|
To string
|
||||||
|
Subject string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new SMTP mail client from config
|
||||||
|
func NewClient() *Client {
|
||||||
|
cfg := config.Cfg
|
||||||
|
return &Client{
|
||||||
|
host: cfg.MailHost,
|
||||||
|
port: cfg.MailPort,
|
||||||
|
username: cfg.MailUsername,
|
||||||
|
password: cfg.MailPassword,
|
||||||
|
from: cfg.MailFrom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send sends an email message via SMTP
|
||||||
|
func (c *Client) Send(msg Message) error {
|
||||||
|
if c.host == "" {
|
||||||
|
logger.Warn("SMTP Host nicht konfiguriert - E-Mail-Versand übersprungen")
|
||||||
|
return fmt.Errorf("SMTP host not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("Sende E-Mail an %s via SMTP %s:%d", msg.To, c.host, c.port)
|
||||||
|
|
||||||
|
// Prepare email headers and body
|
||||||
|
headers := make(map[string]string)
|
||||||
|
headers["From"] = c.from
|
||||||
|
headers["To"] = msg.To
|
||||||
|
headers["Subject"] = msg.Subject
|
||||||
|
headers["MIME-Version"] = "1.0"
|
||||||
|
headers["Content-Type"] = "text/html; charset=\"UTF-8\""
|
||||||
|
|
||||||
|
// Build message
|
||||||
|
message := ""
|
||||||
|
for key, value := range headers {
|
||||||
|
message += fmt.Sprintf("%s: %s\r\n", key, value)
|
||||||
|
}
|
||||||
|
message += "\r\n" + msg.Body
|
||||||
|
|
||||||
|
// Connect to SMTP server
|
||||||
|
serverAddr := fmt.Sprintf("%s:%d", c.host, c.port)
|
||||||
|
|
||||||
|
// Create TLS config
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
ServerName: c.host,
|
||||||
|
InsecureSkipVerify: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var client *smtp.Client
|
||||||
|
|
||||||
|
// Port 465 requires direct TLS connection, port 587 uses STARTTLS
|
||||||
|
if c.port == 465 {
|
||||||
|
// Direct TLS connection (implicit SSL)
|
||||||
|
conn, err := tls.Dial("tcp", serverAddr, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Fehler beim Verbinden mit SMTP-Server (TLS): %s", err.Error())
|
||||||
|
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client, err = smtp.NewClient(conn, c.host)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Fehler beim Erstellen des SMTP-Clients: %s", err.Error())
|
||||||
|
return fmt.Errorf("failed to create SMTP client: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Port 587 or other - use STARTTLS
|
||||||
|
client, err = smtp.Dial(serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Fehler beim Verbinden mit SMTP-Server: %s", err.Error())
|
||||||
|
return fmt.Errorf("failed to dial SMTP server: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Start TLS if available
|
||||||
|
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||||
|
if err = client.StartTLS(tlsConfig); err != nil {
|
||||||
|
logger.Error("Fehler beim STARTTLS: %s", err.Error())
|
||||||
|
return fmt.Errorf("failed to start TLS: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate
|
||||||
|
if c.username != "" && c.password != "" {
|
||||||
|
auth := smtp.PlainAuth("", c.username, c.password, c.host)
|
||||||
|
if err = client.Auth(auth); err != nil {
|
||||||
|
logger.Error("Fehler bei der SMTP-Authentifizierung: %s", err.Error())
|
||||||
|
return fmt.Errorf("failed to authenticate: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sender
|
||||||
|
if err = client.Mail(c.from); err != nil {
|
||||||
|
logger.Error("Fehler beim Setzen des Absenders: %s", err.Error())
|
||||||
|
return fmt.Errorf("failed to set sender: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set recipient
|
||||||
|
if err = client.Rcpt(msg.To); err != nil {
|
||||||
|
logger.Error("Fehler beim Setzen des Empfängers: %s", err.Error())
|
||||||
|
return fmt.Errorf("failed to set recipient: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send message body
|
||||||
|
writer, err := client.Data()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Fehler beim Öffnen des Data-Writers: %s", err.Error())
|
||||||
|
return fmt.Errorf("failed to open data writer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = writer.Write([]byte(message))
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Fehler beim Schreiben der Nachricht: %s", err.Error())
|
||||||
|
writer.Close()
|
||||||
|
return fmt.Errorf("failed to write message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = writer.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Fehler beim Schließen des Data-Writers: %s", err.Error())
|
||||||
|
return fmt.Errorf("failed to close data writer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit
|
||||||
|
if err = client.Quit(); err != nil {
|
||||||
|
logger.Error("Fehler beim Beenden der SMTP-Verbindung: %s", err.Error())
|
||||||
|
return fmt.Errorf("failed to quit SMTP connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("E-Mail erfolgreich an %s versendet", msg.To)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsConfigured returns true if the mail client is properly configured
|
||||||
|
func (c *Client) IsConfigured() bool {
|
||||||
|
return c.host != "" && c.port > 0 && c.from != ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"bamort/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupTestEnvironment setzt die Test-Umgebung auf
|
||||||
|
func setupTestEnvironment(t *testing.T) {
|
||||||
|
original := os.Getenv("ENVIRONMENT")
|
||||||
|
os.Setenv("ENVIRONMENT", "test")
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if original != "" {
|
||||||
|
os.Setenv("ENVIRONMENT", original)
|
||||||
|
} else {
|
||||||
|
os.Unsetenv("ENVIRONMENT")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Reload config with test environment
|
||||||
|
config.Cfg = config.LoadConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewClient(t *testing.T) {
|
||||||
|
setupTestEnvironment(t)
|
||||||
|
|
||||||
|
// Set test mail config
|
||||||
|
os.Setenv("MAIL_HOST", "smtp.example.com")
|
||||||
|
os.Setenv("MAIL_PORT", "465")
|
||||||
|
os.Setenv("MAIL_USERNAME", "test@example.com")
|
||||||
|
os.Setenv("MAIL_PASSWORD", "testpass")
|
||||||
|
os.Setenv("MAIL_FROM", "sender@example.com")
|
||||||
|
|
||||||
|
// Reload config
|
||||||
|
config.Cfg = config.LoadConfig()
|
||||||
|
|
||||||
|
client := NewClient()
|
||||||
|
|
||||||
|
if client.host != "smtp.example.com" {
|
||||||
|
t.Errorf("Expected host 'smtp.example.com', got '%s'", client.host)
|
||||||
|
}
|
||||||
|
if client.port != 465 {
|
||||||
|
t.Errorf("Expected port 465, got %d", client.port)
|
||||||
|
}
|
||||||
|
if client.username != "test@example.com" {
|
||||||
|
t.Errorf("Expected username 'test@example.com', got '%s'", client.username)
|
||||||
|
}
|
||||||
|
if client.from != "sender@example.com" {
|
||||||
|
t.Errorf("Expected from 'sender@example.com', got '%s'", client.from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsConfigured(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
client *Client
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Fully configured",
|
||||||
|
client: &Client{
|
||||||
|
host: "smtp.example.com",
|
||||||
|
port: 465,
|
||||||
|
from: "test@example.com",
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing host",
|
||||||
|
client: &Client{
|
||||||
|
host: "",
|
||||||
|
port: 465,
|
||||||
|
from: "test@example.com",
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing port",
|
||||||
|
client: &Client{
|
||||||
|
host: "smtp.example.com",
|
||||||
|
port: 0,
|
||||||
|
from: "test@example.com",
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing from",
|
||||||
|
client: &Client{
|
||||||
|
host: "smtp.example.com",
|
||||||
|
port: 465,
|
||||||
|
from: "",
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty client",
|
||||||
|
client: &Client{
|
||||||
|
host: "",
|
||||||
|
port: 0,
|
||||||
|
from: "",
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := tt.client.IsConfigured()
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("Expected IsConfigured() to return %v, got %v", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSend_NotConfigured(t *testing.T) {
|
||||||
|
setupTestEnvironment(t)
|
||||||
|
|
||||||
|
// Create client without config
|
||||||
|
client := &Client{
|
||||||
|
host: "",
|
||||||
|
port: 0,
|
||||||
|
from: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := Message{
|
||||||
|
To: "recipient@example.com",
|
||||||
|
Subject: "Test",
|
||||||
|
Body: "Test body",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.Send(msg)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when sending with unconfigured client, got nil")
|
||||||
|
}
|
||||||
|
if err.Error() != "SMTP host not configured" {
|
||||||
|
t.Errorf("Expected error 'SMTP host not configured', got '%s'", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
+75
-19
@@ -7,6 +7,7 @@ package user
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bamort/logger"
|
"bamort/logger"
|
||||||
|
"bamort/mail"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -239,8 +240,7 @@ func generateResetHash() (string, error) {
|
|||||||
return hex.EncodeToString(bytes), nil
|
return hex.EncodeToString(bytes), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendResetEmail simuliert das Senden einer E-Mail (hier nur Logging)
|
// sendResetEmail sends a password reset email via SMTP
|
||||||
// In einer echten Implementierung würde hier ein E-Mail-Service verwendet
|
|
||||||
func sendResetEmail(email, username, resetHash, frontendURL string) error {
|
func sendResetEmail(email, username, resetHash, frontendURL string) error {
|
||||||
// Verwende die mitgegebene Frontend-URL oder fallback auf Standard
|
// Verwende die mitgegebene Frontend-URL oder fallback auf Standard
|
||||||
baseURL := frontendURL
|
baseURL := frontendURL
|
||||||
@@ -250,25 +250,81 @@ func sendResetEmail(email, username, resetHash, frontendURL string) error {
|
|||||||
|
|
||||||
resetLink := fmt.Sprintf("%s/reset-password?token=%s", baseURL, resetHash)
|
resetLink := fmt.Sprintf("%s/reset-password?token=%s", baseURL, resetHash)
|
||||||
|
|
||||||
logger.Info("=== PASSWORD RESET EMAIL ===")
|
// Create mail client
|
||||||
logger.Info("An: %s", email)
|
mailClient := mail.NewClient()
|
||||||
logger.Info("Betreff: Passwort zurücksetzen für %s", username)
|
|
||||||
logger.Info("Nachricht:")
|
|
||||||
logger.Info("Hallo %s,", username)
|
|
||||||
logger.Info("")
|
|
||||||
logger.Info("Sie haben eine Passwort-Zurücksetzung angefordert.")
|
|
||||||
logger.Info("Klicken Sie auf den folgenden Link, um Ihr Passwort zurückzusetzen:")
|
|
||||||
logger.Info("")
|
|
||||||
logger.Info("%s", resetLink)
|
|
||||||
logger.Info("")
|
|
||||||
logger.Info("Dieser Link ist 14 Tage gültig.")
|
|
||||||
logger.Info("Falls Sie diese Anfrage nicht gestellt haben, ignorieren Sie diese E-Mail.")
|
|
||||||
logger.Info("")
|
|
||||||
logger.Info("=== END EMAIL ===")
|
|
||||||
|
|
||||||
// TODO: Hier echte E-Mail-Integration hinzufügen
|
// If mail is not configured, fallback to logging
|
||||||
// z.B. SendGrid, SMTP, etc.
|
if !mailClient.IsConfigured() {
|
||||||
|
logger.Warn("SMTP nicht konfiguriert - E-Mail wird nur geloggt")
|
||||||
|
logger.Info("=== PASSWORD RESET EMAIL ===")
|
||||||
|
logger.Info("An: %s", email)
|
||||||
|
logger.Info("Betreff: Passwort zurücksetzen für %s", username)
|
||||||
|
logger.Info("Nachricht:")
|
||||||
|
logger.Info("Hallo %s,", username)
|
||||||
|
logger.Info("")
|
||||||
|
logger.Info("Sie haben eine Passwort-Zurücksetzung angefordert.")
|
||||||
|
logger.Info("Klicken Sie auf den folgenden Link, um Ihr Passwort zurückzusetzen:")
|
||||||
|
logger.Info("")
|
||||||
|
logger.Info("%s", resetLink)
|
||||||
|
logger.Info("")
|
||||||
|
logger.Info("Dieser Link ist 14 Tage gültig.")
|
||||||
|
logger.Info("Falls Sie diese Anfrage nicht gestellt haben, ignorieren Sie diese E-Mail.")
|
||||||
|
logger.Info("")
|
||||||
|
logger.Info("=== END EMAIL ===")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build HTML email body
|
||||||
|
htmlBody := fmt.Sprintf(`<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.header { background-color: #4CAF50; color: white; padding: 20px; text-align: center; }
|
||||||
|
.content { background-color: #f9f9f9; padding: 30px; margin-top: 20px; }
|
||||||
|
.button { display: inline-block; padding: 12px 30px; margin: 20px 0; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 5px; }
|
||||||
|
.footer { margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd; font-size: 12px; color: #666; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>Passwort zurücksetzen</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>Hallo %s,</p>
|
||||||
|
<p>Sie haben eine Passwort-Zurücksetzung für Ihren Bamort-Account angefordert.</p>
|
||||||
|
<p>Klicken Sie auf den folgenden Button, um Ihr Passwort zurückzusetzen:</p>
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<a href="%s" class="button">Passwort zurücksetzen</a>
|
||||||
|
</p>
|
||||||
|
<p>Oder kopieren Sie diesen Link in Ihren Browser:</p>
|
||||||
|
<p style="word-break: break-all; color: #666;">%s</p>
|
||||||
|
<p><strong>Dieser Link ist 14 Tage gültig.</strong></p>
|
||||||
|
<p>Falls Sie diese Anfrage nicht gestellt haben, können Sie diese E-Mail ignorieren. Ihr Passwort bleibt unverändert.</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>Dies ist eine automatisch generierte E-Mail. Bitte antworten Sie nicht auf diese Nachricht.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`, username, resetLink, resetLink)
|
||||||
|
|
||||||
|
// Send email
|
||||||
|
msg := mail.Message{
|
||||||
|
To: email,
|
||||||
|
Subject: "Passwort zurücksetzen - Bamort",
|
||||||
|
Body: htmlBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mailClient.Send(msg); err != nil {
|
||||||
|
logger.Error("Fehler beim Versenden der Reset-E-Mail an %s: %s", email, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Password-Reset-E-Mail erfolgreich an %s versendet", email)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -18,4 +18,10 @@ API_URL=http://192.168.0.48:8180
|
|||||||
API_PORT=8180
|
API_PORT=8180
|
||||||
BASE_URL=http://localhost:5173
|
BASE_URL=http://localhost:5173
|
||||||
TEMPLATES_DIR=./templates
|
TEMPLATES_DIR=./templates
|
||||||
EXPORT_TEMP_DIR=./export_temp
|
EXPORT_TEMP_DIR=./export_temp
|
||||||
|
|
||||||
|
# Mail Configuration (for development)
|
||||||
|
MAIL_HOST=mail.wuenscheonline.de
|
||||||
|
MAIL_PORT=465
|
||||||
|
MAIL_USERNAME=bamort@trokan.de
|
||||||
|
MAIL_PASSWORD=XXXmhqbv.DW+XXX
|
||||||
|
|||||||
@@ -18,3 +18,9 @@ API_PORT=8180
|
|||||||
BASE_URL=https://bamort.trokan.de
|
BASE_URL=https://bamort.trokan.de
|
||||||
TEMPLATES_DIR=./templates
|
TEMPLATES_DIR=./templates
|
||||||
EXPORT_TEMP_DIR=./export_temp
|
EXPORT_TEMP_DIR=./export_temp
|
||||||
|
|
||||||
|
# Mail Configuration (for production)
|
||||||
|
MAIL_HOST=mail.wuenscheonline.de
|
||||||
|
MAIL_PORT=465
|
||||||
|
MAIL_USERNAME=bamort@trokan.de
|
||||||
|
MAIL_PASSWORD=XXXXmhqbv.DW+XXX
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Frontend Version Management
|
# Frontend Version Management
|
||||||
|
|
||||||
## Current Version: 0.2.2
|
## Current Version: 0.2.3
|
||||||
|
|
||||||
The frontend version is managed independently from the backend.
|
The frontend version is managed independently from the backend.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bamort-frontend",
|
"name": "bamort-frontend",
|
||||||
"version": "0.2.2",
|
"version": "0.2.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1674,7 +1674,7 @@ a:focus {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 1000;
|
z-index: 1100;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal content container */
|
/* Modal content container */
|
||||||
@@ -1727,11 +1727,13 @@ a:focus {
|
|||||||
/* Modal actions (alternative to footer) */
|
/* Modal actions (alternative to footer) */
|
||||||
.modal-actions {
|
.modal-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 20px;
|
padding: 20px 24px;
|
||||||
padding-top: 15px;
|
border-top: 1px solid #dee2e6;
|
||||||
border-top: 1px solid #eee;
|
background: #f8f9fa;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Close button */
|
/* Close button */
|
||||||
@@ -1778,6 +1780,8 @@ a:focus {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========================================
|
/* ========================================
|
||||||
@@ -4403,3 +4407,23 @@ a:focus {
|
|||||||
max-height: 150px;
|
max-height: 150px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
@@ -549,7 +549,7 @@ export default {
|
|||||||
color: #666;
|
color: #666;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
.help-icon {
|
.help-icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -561,7 +561,6 @@ export default {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
/*cursor: help;*/
|
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
@@ -570,4 +569,5 @@ export default {
|
|||||||
background: #e0e0e0;
|
background: #e0e0e0;
|
||||||
color: #222;
|
color: #222;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -36,6 +36,14 @@
|
|||||||
<!-- Character Information -->
|
<!-- Character Information -->
|
||||||
<div class="character-info">
|
<div class="character-info">
|
||||||
<div class="info-section">
|
<div class="info-section">
|
||||||
|
<label for="name"><span
|
||||||
|
class="help-icon"
|
||||||
|
:title="$t('characters.datasheet.editHelp')"
|
||||||
|
role="img"
|
||||||
|
:aria-label="$t('characters.datasheet.editHelp')"
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</span></label>
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ $t('char') }}:</strong>
|
<strong>{{ $t('char') }}:</strong>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -36,13 +36,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button @click="closeDialog" class="btn-cancel" :disabled="isExporting">
|
<button @click="performExport" class="btn-primary btn-save" :disabled="!canExport || isExporting">
|
||||||
{{ $t('export.cancel') }}
|
|
||||||
</button>
|
|
||||||
<button @click="performExport" class="btn-export" :disabled="!canExport || isExporting">
|
|
||||||
<span v-if="!isExporting">{{ $t('export.export') }}</span>
|
<span v-if="!isExporting">{{ $t('export.export') }}</span>
|
||||||
<span v-else>{{ $t('export.exporting') }}</span>
|
<span v-else>{{ $t('export.exporting') }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button @click="closeDialog" class="btn-cancel" :disabled="isExporting">
|
||||||
|
{{ $t('common.cancel') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,57 +1,56 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fullwidth-page" style="display: flex; justify-content: center; align-items: center; min-height: 100vh;">
|
<div class="fullwidth-page reset-wrapper">
|
||||||
<div class="card" style="max-width: 400px; width: 100%; margin: 20px;">
|
<div class="card reset-card">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>Passwort zurücksetzen</h2>
|
<h2>{{ $t('forgotPassword.title') }}</h2>
|
||||||
<p style="color: #666; font-size: 0.9em; margin-top: 10px;">
|
<p class="reset-description">
|
||||||
Geben Sie Ihre E-Mail-Adresse ein, um einen Reset-Link zu erhalten.
|
{{ $t('forgotPassword.description') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form @submit.prevent="requestReset" v-if="!submitted">
|
<form @submit.prevent="requestReset" v-if="!submitted">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">E-Mail-Adresse</label>
|
<label for="email">{{ $t('forgotPassword.emailLabel') }}</label>
|
||||||
<input
|
<input
|
||||||
v-model="email"
|
v-model="email"
|
||||||
type="email"
|
type="email"
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="ihre@email.de"
|
:placeholder="$t('forgotPassword.emailPlaceholder')"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary reset-button"
|
||||||
style="width: 100%; margin-top: 10px;"
|
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
>
|
>
|
||||||
<span v-if="isLoading">Wird gesendet...</span>
|
<span v-if="isLoading">{{ $t('forgotPassword.submitting') }}</span>
|
||||||
<span v-else>Reset-Link senden</span>
|
<span v-else>{{ $t('forgotPassword.submit') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div v-if="submitted" class="badge badge-success" style="width: 100%; margin-top: 15px; text-align: center; display: block;">
|
<div v-if="submitted" class="badge badge-success reset-badge">
|
||||||
<p style="margin: 10px 0;">
|
<p class="reset-success-title">
|
||||||
<strong>E-Mail gesendet!</strong>
|
<strong>{{ $t('forgotPassword.successTitle') }}</strong>
|
||||||
</p>
|
</p>
|
||||||
<p style="font-size: 0.9em; margin: 5px 0;">
|
<p class="reset-success-text">
|
||||||
Falls ein Account mit dieser E-Mail-Adresse existiert, wurde ein Reset-Link gesendet.
|
{{ $t('forgotPassword.successInfo') }}
|
||||||
</p>
|
</p>
|
||||||
<p style="font-size: 0.8em; margin: 5px 0; opacity: 0.8;">
|
<p class="reset-success-hint">
|
||||||
Prüfen Sie Ihre E-Mails und folgen Sie dem Link.
|
{{ $t('forgotPassword.successHint') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="badge badge-danger" style="width: 100%; margin-top: 15px; text-align: center; display: block;">
|
<div v-if="error" class="badge badge-danger reset-badge">
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align: center; margin-top: 20px; padding-top: 15px; border-top: 1px solid #dee2e6;">
|
<div class="reset-footer">
|
||||||
<router-link to="/" class="btn btn-secondary">
|
<router-link to="/" class="btn btn-secondary">
|
||||||
Zurück zum Login
|
{{ $t('forgotPassword.backToLogin') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,7 +85,7 @@ export default {
|
|||||||
console.log('Password reset email requested for:', this.email)
|
console.log('Password reset email requested for:', this.email)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Password reset request error:', err)
|
console.error('Password reset request error:', err)
|
||||||
this.error = err.response?.data?.error || 'Fehler beim Senden der E-Mail. Versuchen Sie es später erneut.'
|
this.error = err.response?.data?.error || this.$t('forgotPassword.error')
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
}
|
}
|
||||||
@@ -95,6 +94,57 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
/* All common styles moved to main.css */
|
.reset-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-card {
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-description {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-badge {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-success-title {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-success-text {
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-success-hint {
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin: 5px 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,7 +3,16 @@
|
|||||||
<div class="cd-list">
|
<div class="cd-list">
|
||||||
<div class="tables-container">
|
<div class="tables-container">
|
||||||
<div class="table-wrapper-left">
|
<div class="table-wrapper-left">
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
|
<span
|
||||||
|
v-if="isOwner"
|
||||||
|
class="help-icon"
|
||||||
|
:title="$t('characters.datasheet.editHelp')"
|
||||||
|
role="img"
|
||||||
|
:aria-label="$t('characters.datasheet.editHelp')"
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</span>
|
||||||
<!-- Lernmodus Toggle Button -->
|
<!-- Lernmodus Toggle Button -->
|
||||||
<div v-if="isOwner" class="learning-mode-controls">
|
<div v-if="isOwner" class="learning-mode-controls">
|
||||||
<!-- Ressourcen-Anzeige (nur sichtbar wenn Lernmodus aktiv) -->
|
<!-- Ressourcen-Anzeige (nur sichtbar wenn Lernmodus aktiv) -->
|
||||||
@@ -64,8 +73,42 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<template v-for="skill in skills">
|
<template v-for="skill in skills">
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ skill.name || '-' }}</td>
|
<td>
|
||||||
<td>{{ skill.fertigkeitswert || '-' }}</td>
|
<span
|
||||||
|
v-if="!isEditingSkill(skill, 'name')"
|
||||||
|
@dblclick="isOwner ? startEditSkill(skill, 'name') : null"
|
||||||
|
:class="{ 'editable-prop': isOwner }"
|
||||||
|
>{{ skill.name || '-' }}</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="editValue"
|
||||||
|
@blur="saveEditSkill(skill, 'name')"
|
||||||
|
@keyup.enter="saveEditSkill(skill, 'name')"
|
||||||
|
@keyup.esc="cancelEditSkill"
|
||||||
|
ref="editInput"
|
||||||
|
class="prop-input"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
v-if="!isEditingSkill(skill, 'fertigkeitswert')"
|
||||||
|
@dblclick="isOwner ? startEditSkill(skill, 'fertigkeitswert') : null"
|
||||||
|
:class="{ 'editable-prop': isOwner }"
|
||||||
|
>{{ skill.fertigkeitswert || '-' }}</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="editValue"
|
||||||
|
@blur="saveEditSkill(skill, 'fertigkeitswert')"
|
||||||
|
@keyup.enter="saveEditSkill(skill, 'fertigkeitswert')"
|
||||||
|
@keyup.esc="cancelEditSkill"
|
||||||
|
ref="editInput"
|
||||||
|
class="prop-input"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="20"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td>{{ skill.bonus || '0' }}</td>
|
<td>{{ skill.bonus || '0' }}</td>
|
||||||
<td class="pp-cell">
|
<td class="pp-cell">
|
||||||
<div v-if="isOwner" class="pp-container">
|
<div v-if="isOwner" class="pp-container">
|
||||||
@@ -88,7 +131,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<span v-else>{{ skill.pp || '0' }}</span>
|
<span v-else>{{ skill.pp || '0' }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ skill.bemerkung || '-' }}</td>
|
<td>
|
||||||
|
<span
|
||||||
|
v-if="!isEditingSkill(skill, 'bemerkung')"
|
||||||
|
@dblclick="isOwner ? startEditSkill(skill, 'bemerkung') : null"
|
||||||
|
:class="{ 'editable-prop': isOwner }"
|
||||||
|
>{{ skill.bemerkung || '-' }}</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="editValue"
|
||||||
|
@blur="saveEditSkill(skill, 'bemerkung')"
|
||||||
|
@keyup.enter="saveEditSkill(skill, 'bemerkung')"
|
||||||
|
@keyup.esc="cancelEditSkill"
|
||||||
|
ref="editInput"
|
||||||
|
class="prop-input"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td v-if="learningMode" class="action-cell">
|
<td v-if="learningMode" class="action-cell">
|
||||||
<button
|
<button
|
||||||
@click="improveSkill(skill)"
|
@click="improveSkill(skill)"
|
||||||
@@ -106,8 +165,42 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<template v-for="skill in character.waffenfertigkeiten">
|
<template v-for="skill in character.waffenfertigkeiten">
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ skill.name || '-' }}</td>
|
<td>
|
||||||
<td>{{ skill.fertigkeitswert || '-' }}</td>
|
<span
|
||||||
|
v-if="!isEditingSkill(skill, 'name')"
|
||||||
|
@dblclick="isOwner ? startEditSkill(skill, 'name') : null"
|
||||||
|
:class="{ 'editable-prop': isOwner }"
|
||||||
|
>{{ skill.name || '-' }}</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="editValue"
|
||||||
|
@blur="saveEditSkill(skill, 'name')"
|
||||||
|
@keyup.enter="saveEditSkill(skill, 'name')"
|
||||||
|
@keyup.esc="cancelEditSkill"
|
||||||
|
ref="editInput"
|
||||||
|
class="prop-input"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
v-if="!isEditingSkill(skill, 'fertigkeitswert')"
|
||||||
|
@dblclick="isOwner ? startEditSkill(skill, 'fertigkeitswert') : null"
|
||||||
|
:class="{ 'editable-prop': isOwner }"
|
||||||
|
>{{ skill.fertigkeitswert || '-' }}</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="editValue"
|
||||||
|
@blur="saveEditSkill(skill, 'fertigkeitswert')"
|
||||||
|
@keyup.enter="saveEditSkill(skill, 'fertigkeitswert')"
|
||||||
|
@keyup.esc="cancelEditSkill"
|
||||||
|
ref="editInput"
|
||||||
|
class="prop-input"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="20"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td>{{ skill.bonus || '0' }}</td>
|
<td>{{ skill.bonus || '0' }}</td>
|
||||||
<td class="pp-cell">
|
<td class="pp-cell">
|
||||||
<div v-if="isOwner" class="pp-container">
|
<div v-if="isOwner" class="pp-container">
|
||||||
@@ -130,7 +223,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<span v-else>{{ skill.pp || '0' }}</span>
|
<span v-else>{{ skill.pp || '0' }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ skill.bemerkung || '-' }}</td>
|
<td>
|
||||||
|
<span
|
||||||
|
v-if="!isEditingSkill(skill, 'bemerkung')"
|
||||||
|
@dblclick="isOwner ? startEditSkill(skill, 'bemerkung') : null"
|
||||||
|
:class="{ 'editable-prop': isOwner }"
|
||||||
|
>{{ skill.bemerkung || '-' }}</span>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="editValue"
|
||||||
|
@blur="saveEditSkill(skill, 'bemerkung')"
|
||||||
|
@keyup.enter="saveEditSkill(skill, 'bemerkung')"
|
||||||
|
@keyup.esc="cancelEditSkill"
|
||||||
|
ref="editInput"
|
||||||
|
class="prop-input"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td v-if="learningMode" class="action-cell">
|
<td v-if="learningMode" class="action-cell">
|
||||||
<button
|
<button
|
||||||
@click="improveWeaponSkill(skill)"
|
@click="improveWeaponSkill(skill)"
|
||||||
@@ -318,6 +427,11 @@ export default {
|
|||||||
selectedSkillToLearn: null,
|
selectedSkillToLearn: null,
|
||||||
selectedLearningType: 'improve', // 'improve', 'learn', 'spell'
|
selectedLearningType: 'improve', // 'improve', 'learn', 'spell'
|
||||||
|
|
||||||
|
// Inline editing
|
||||||
|
editingSkillId: null,
|
||||||
|
editingField: null,
|
||||||
|
editValue: '',
|
||||||
|
|
||||||
isLoading: false
|
isLoading: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -623,6 +737,72 @@ export default {
|
|||||||
skill.pp = ppMap[skill.name] || 0;
|
skill.pp = ppMap[skill.name] || 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Inline editing methods
|
||||||
|
getSkillId(skill) {
|
||||||
|
// Create unique ID combining name and type
|
||||||
|
return `${skill.name}_${skill.kategorie || 'weapon'}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
startEditSkill(skill, field) {
|
||||||
|
if (!this.isOwner) return;
|
||||||
|
|
||||||
|
this.editingSkillId = this.getSkillId(skill);
|
||||||
|
this.editingField = field;
|
||||||
|
this.editValue = skill[field] || '';
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const input = this.$refs.editInput;
|
||||||
|
if (input) {
|
||||||
|
const element = Array.isArray(input) ? input[0] : input;
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
if (element.select) element.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveEditSkill(skill, field) {
|
||||||
|
if (this.editingSkillId === null) return;
|
||||||
|
|
||||||
|
let newValue = this.editValue;
|
||||||
|
|
||||||
|
// Validate and convert for fertigkeitswert
|
||||||
|
if (field === 'fertigkeitswert') {
|
||||||
|
newValue = parseInt(this.editValue);
|
||||||
|
if (isNaN(newValue) || newValue < 0 || newValue > 20) {
|
||||||
|
alert('Fertigkeitswert muss zwischen 0 und 20 liegen.');
|
||||||
|
this.cancelEditSkill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local character object
|
||||||
|
skill[field] = newValue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Save to backend
|
||||||
|
await API.put(`/api/characters/${this.character.id}`, this.character);
|
||||||
|
|
||||||
|
this.$emit('character-updated');
|
||||||
|
this.cancelEditSkill();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update skill:', error);
|
||||||
|
alert('Fehler beim Speichern: ' + (error.response?.data?.error || error.message));
|
||||||
|
this.cancelEditSkill();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelEditSkill() {
|
||||||
|
this.editingSkillId = null;
|
||||||
|
this.editingField = null;
|
||||||
|
this.editValue = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
isEditingSkill(skill, field) {
|
||||||
|
return this.editingSkillId === this.getSkillId(skill) && this.editingField === field;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -111,12 +111,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button @click="closeDialog" class="btn-cancel" :disabled="isUpdating">
|
<button @click="updateVisibilityAndShares" class="btn-primary btn-save" :disabled="isUpdating">
|
||||||
{{ $t('visibility.cancel') }}
|
<span v-if="!isUpdating">{{ $t('common.save') }}</span>
|
||||||
|
<span v-else>{{ $t('common.saving') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="updateVisibilityAndShares" class="btn-primary" :disabled="isUpdating">
|
<button @click="closeDialog" class="btn-cancel" :disabled="isUpdating">
|
||||||
<span v-if="!isUpdating">{{ $t('visibility.save') }}</span>
|
{{ $t('common.cancel') }}
|
||||||
<span v-else>{{ $t('visibility.saving') }}</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('search')"
|
:placeholder="$t('search')"
|
||||||
/>
|
/>
|
||||||
|
<button class="btn-primary" @click="startCreate">{{ $t('newEntry') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -31,6 +32,65 @@
|
|||||||
<td colspan="7">{{ $t('common.loading') }}</td>
|
<td colspan="7">{{ $t('common.loading') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr v-if="creatingNew">
|
||||||
|
<td>New</td>
|
||||||
|
<td colspan="6">
|
||||||
|
<div class="edit-form">
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('believe.name') }}</label>
|
||||||
|
<input v-model="newItem.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('believe.description') }}</label>
|
||||||
|
<input v-model="newItem.beschreibung" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('believe.source') }}</label>
|
||||||
|
<select v-model="newItem.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="newItem.page_number" type="number" min="0" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('believe.system') }}</label>
|
||||||
|
<select v-model.number="createSelectedSystemId">
|
||||||
|
<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 btn-save"
|
||||||
|
|
||||||
|
:disabled="isSaving"
|
||||||
|
@click="saveCreate"
|
||||||
|
>
|
||||||
|
<span v-if="!isSaving">{{ $t('common.save') }}</span>
|
||||||
|
<span v-else>{{ $t('common.saving') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn-cancel"
|
||||||
|
:disabled="isSaving"
|
||||||
|
@click="cancelCreate"
|
||||||
|
>
|
||||||
|
{{ $t('common.cancel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<template v-for="believe in filteredBelieves" :key="believe.id">
|
<template v-for="believe in filteredBelieves" :key="believe.id">
|
||||||
<tr v-if="editingId !== believe.id">
|
<tr v-if="editingId !== believe.id">
|
||||||
<td>{{ believe.id }}</td>
|
<td>{{ believe.id }}</td>
|
||||||
@@ -40,7 +100,7 @@
|
|||||||
<td>{{ believe.page_number || '-' }}</td>
|
<td>{{ believe.page_number || '-' }}</td>
|
||||||
<td>{{ getSystemCodeById(believe.game_system_id, believe.game_system) || '-' }}</td>
|
<td>{{ getSystemCodeById(believe.game_system_id, believe.game_system) || '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button @click="startEdit(believe)">{{ $t('believe.edit') }}</button>
|
<button @click="startEdit(believe)">{{ $t('common.edit') }}</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else>
|
<tr v-else>
|
||||||
@@ -82,19 +142,19 @@
|
|||||||
|
|
||||||
<div class="edit-actions">
|
<div class="edit-actions">
|
||||||
<button
|
<button
|
||||||
class="btn-primary"
|
class="btn-primary btn-save"
|
||||||
:disabled="isSaving"
|
:disabled="isSaving"
|
||||||
@click="saveEdit"
|
@click="saveEdit"
|
||||||
>
|
>
|
||||||
<span v-if="!isSaving">{{ $t('believe.save') }}</span>
|
<span v-if="!isSaving">{{ $t('common.save') }}</span>
|
||||||
<span v-else>{{ $t('believe.saving') }}</span>
|
<span v-else>{{ $t('common.saving') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn-cancel"
|
class="btn-cancel"
|
||||||
:disabled="isSaving"
|
:disabled="isSaving"
|
||||||
@click="cancelEdit"
|
@click="cancelEdit"
|
||||||
>
|
>
|
||||||
{{ $t('believe.cancel') }}
|
{{ $t('common.cancel') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -179,6 +239,9 @@ export default {
|
|||||||
editedItem: null,
|
editedItem: null,
|
||||||
gameSystems: [],
|
gameSystems: [],
|
||||||
selectedSystemId: null,
|
selectedSystemId: null,
|
||||||
|
creatingNew: false,
|
||||||
|
newItem: null,
|
||||||
|
createSelectedSystemId: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
error: '',
|
error: '',
|
||||||
@@ -251,6 +314,23 @@ export default {
|
|||||||
findSystemIdByCode(code) {
|
findSystemIdByCode(code) {
|
||||||
return findSystemIdByCode(this.gameSystems, code)
|
return findSystemIdByCode(this.gameSystems, code)
|
||||||
},
|
},
|
||||||
|
startCreate() {
|
||||||
|
this.cancelEdit()
|
||||||
|
const defaultSystem = this.gameSystems.find(gs => gs.is_active) || this.gameSystems[0] || null
|
||||||
|
this.createSelectedSystemId = defaultSystem ? defaultSystem.id : null
|
||||||
|
this.newItem = {
|
||||||
|
name: '',
|
||||||
|
beschreibung: '',
|
||||||
|
sourceCode: '',
|
||||||
|
page_number: 0
|
||||||
|
}
|
||||||
|
this.creatingNew = true
|
||||||
|
},
|
||||||
|
cancelCreate() {
|
||||||
|
this.creatingNew = false
|
||||||
|
this.newItem = null
|
||||||
|
this.createSelectedSystemId = null
|
||||||
|
},
|
||||||
async saveEdit() {
|
async saveEdit() {
|
||||||
if (!this.editedItem || !this.editingId) {
|
if (!this.editedItem || !this.editingId) {
|
||||||
return
|
return
|
||||||
@@ -294,6 +374,45 @@ export default {
|
|||||||
} finally {
|
} finally {
|
||||||
this.isSaving = false
|
this.isSaving = false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async saveCreate() {
|
||||||
|
if (!this.newItem) return
|
||||||
|
|
||||||
|
const trimmedName = (this.newItem.name || '').trim()
|
||||||
|
if (!trimmedName) {
|
||||||
|
alert(this.$t('believe.nameRequired'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedSource = this.sources.find(src => src.code === this.newItem.sourceCode)
|
||||||
|
const selectedSystem = this.gameSystems.find(gs => gs.id === this.createSelectedSystemId)
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: trimmedName,
|
||||||
|
beschreibung: this.newItem.beschreibung || '',
|
||||||
|
source_id: selectedSource ? selectedSource.id : null,
|
||||||
|
page_number: this.newItem.page_number || 0,
|
||||||
|
game_system_id: selectedSystem ? selectedSystem.id : null,
|
||||||
|
game_system: selectedSystem ? selectedSystem.code : '',
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isSaving = true
|
||||||
|
try {
|
||||||
|
const response = await API.post('/api/maintenance/gsm-believes', payload)
|
||||||
|
const created = response.data
|
||||||
|
this.believes.push({
|
||||||
|
...created,
|
||||||
|
source_code: this.getSourceCode(created.source_id),
|
||||||
|
game_system: selectedSystem ? selectedSystem.code : created.game_system,
|
||||||
|
game_system_id: selectedSystem ? selectedSystem.id : (created.game_system_id ?? null)
|
||||||
|
})
|
||||||
|
this.cancelCreate()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to create believe:', err)
|
||||||
|
this.error = err.response?.data?.error || err.message
|
||||||
|
} finally {
|
||||||
|
this.isSaving = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
v-model="searchTerm"
|
v-model="searchTerm"
|
||||||
:placeholder="`${$t('search')} ${$t('Equipment')}...`"
|
:placeholder="`${$t('search')} ${$t('Equipment')}...`"
|
||||||
/>
|
/>
|
||||||
|
<button @click="startCreate" class="btn-primary">{{ $t('newEntry') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -52,6 +53,68 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr v-if="creatingNew">
|
||||||
|
<td>New</td>
|
||||||
|
<td colspan="8">
|
||||||
|
<div class="edit-form">
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('equipment.name') }}:</label>
|
||||||
|
<input v-model="newItem.name" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('equipment.gewicht') }}:</label>
|
||||||
|
<input v-model.number="newItem.gewicht" type="number" style="width:80px;" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('equipment.wert') }}:</label>
|
||||||
|
<input v-model="newItem.wert" style="width:100px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field full-width">
|
||||||
|
<label>{{ $t('equipment.description') }}:</label>
|
||||||
|
<input v-model="newItem.beschreibung" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('equipment.quelle') }}:</label>
|
||||||
|
<select v-model="newItem.sourceCode" style="width:100px;">
|
||||||
|
<option value="">-</option>
|
||||||
|
<option v-for="source in availableSources" :key="source.code" :value="source.code">
|
||||||
|
{{ source.code }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('equipment.page') || 'Page' }}:</label>
|
||||||
|
<input v-model.number="newItem.page_number" type="number" style="width:60px;" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('equipment.personal_item') }}:</label>
|
||||||
|
<input type="checkbox" v-model="newItem.personal_item" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('equipment.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-actions">
|
||||||
|
<button @click="saveCreate" class="btn-save">{{ $t('common.save') }}</button>
|
||||||
|
<button @click="cancelCreate" class="btn-cancel">{{ $t('common.cancel') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<template v-for="(dtaItem, index) in filteredAndSortedEquipments" :key="dtaItem.id">
|
<template v-for="(dtaItem, index) in filteredAndSortedEquipments" :key="dtaItem.id">
|
||||||
<tr v-if="editingIndex !== index">
|
<tr v-if="editingIndex !== index">
|
||||||
<td>{{ dtaItem.id || '' }}</td>
|
<td>{{ dtaItem.id || '' }}</td>
|
||||||
@@ -64,7 +127,7 @@
|
|||||||
<td><input type="checkbox" :checked="dtaItem.personal_item" disabled /></td>
|
<td><input type="checkbox" :checked="dtaItem.personal_item" disabled /></td>
|
||||||
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.system || 'midgard') }}</td>
|
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.system || 'midgard') }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button @click="startEdit(index)">Edit</button>
|
<button @click="startEdit(index)">{{ $t('common.edit') }}</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Edit Mode -->
|
<!-- Edit Mode -->
|
||||||
@@ -125,8 +188,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="edit-actions">
|
<div class="edit-actions">
|
||||||
<button @click="saveEdit(index)" class="btn-save">Save</button>
|
<button @click="saveEdit(index)" class="btn-save">{{ $t('common.save') }}</button>
|
||||||
<button @click="cancelEdit" class="btn-cancel">Cancel</button>
|
<button @click="cancelEdit" class="btn-cancel">{{ $t('common.cancel') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -216,7 +279,10 @@ export default {
|
|||||||
enhancedEquipment: [],
|
enhancedEquipment: [],
|
||||||
availableSources: [],
|
availableSources: [],
|
||||||
gameSystems: [],
|
gameSystems: [],
|
||||||
selectedSystemId: null
|
selectedSystemId: null,
|
||||||
|
creatingNew: false,
|
||||||
|
newItem: null,
|
||||||
|
createSelectedSystemId: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
@@ -352,6 +418,57 @@ export default {
|
|||||||
this.editedItem = null;
|
this.editedItem = null;
|
||||||
this.selectedSystemId = null;
|
this.selectedSystemId = null;
|
||||||
},
|
},
|
||||||
|
startCreate() {
|
||||||
|
this.cancelEdit()
|
||||||
|
const defaultSystem = this.gameSystems.find(gs => gs.is_active) || this.gameSystems[0] || null
|
||||||
|
this.createSelectedSystemId = defaultSystem ? defaultSystem.id : null
|
||||||
|
this.newItem = {
|
||||||
|
name: '',
|
||||||
|
gewicht: 0,
|
||||||
|
wert: '',
|
||||||
|
beschreibung: '',
|
||||||
|
sourceCode: '',
|
||||||
|
page_number: 0,
|
||||||
|
personal_item: false,
|
||||||
|
system: defaultSystem ? defaultSystem.code : ''
|
||||||
|
}
|
||||||
|
this.creatingNew = true
|
||||||
|
},
|
||||||
|
cancelCreate() {
|
||||||
|
this.creatingNew = false
|
||||||
|
this.newItem = null
|
||||||
|
this.createSelectedSystemId = null
|
||||||
|
},
|
||||||
|
async saveCreate() {
|
||||||
|
if (!this.newItem) return
|
||||||
|
try {
|
||||||
|
const source = this.availableSources.find(s => s.code === this.newItem.sourceCode)
|
||||||
|
const selectedSystem = this.gameSystems.find(gs => gs.id === this.createSelectedSystemId)
|
||||||
|
|
||||||
|
const createData = {
|
||||||
|
name: this.newItem.name,
|
||||||
|
gewicht: this.newItem.gewicht ?? 0,
|
||||||
|
wert: this.newItem.wert || '',
|
||||||
|
beschreibung: this.newItem.beschreibung || '',
|
||||||
|
source_id: source ? source.id : null,
|
||||||
|
page_number: this.newItem.page_number || 0,
|
||||||
|
personal_item: !!this.newItem.personal_item,
|
||||||
|
system: selectedSystem ? selectedSystem.code : (this.newItem.system || ''),
|
||||||
|
game_system_id: selectedSystem ? selectedSystem.id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await API.post(
|
||||||
|
'/api/maintenance/equipment-enhanced',
|
||||||
|
createData
|
||||||
|
)
|
||||||
|
|
||||||
|
this.enhancedEquipment.push(response.data)
|
||||||
|
this.cancelCreate()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create equipment:', error)
|
||||||
|
alert('Failed to create equipment: ' + (error.response?.data?.error || error.message))
|
||||||
|
}
|
||||||
|
},
|
||||||
findSystemIdByCode(code) {
|
findSystemIdByCode(code) {
|
||||||
return findSystemIdByCode(this.gameSystems, code)
|
return findSystemIdByCode(this.gameSystems, code)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<h2>{{ $t('maintenance') }} - {{ $t('gamesystem.title') }}</h2>
|
<h2>{{ $t('maintenance') }} - {{ $t('gamesystem.title') }}</h2>
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<input v-model="searchTerm" type="text" :placeholder="$t('search')" />
|
<input v-model="searchTerm" type="text" :placeholder="$t('search')" />
|
||||||
|
<button class="btn-primary" @click="startCreate">{{ $t('newEntry') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -26,6 +27,39 @@
|
|||||||
<td colspan="6">{{ $t('common.loading') }}</td>
|
<td colspan="6">{{ $t('common.loading') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr v-if="creatingNew">
|
||||||
|
<td>New</td>
|
||||||
|
<td colspan="5">
|
||||||
|
<div class="edit-form">
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('gamesystem.code') }}</label>
|
||||||
|
<input v-model="newItem.code" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('gamesystem.name') }}</label>
|
||||||
|
<input v-model="newItem.name" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('gamesystem.description') }}</label>
|
||||||
|
<input v-model="newItem.description" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('gamesystem.active') }}</label>
|
||||||
|
<input type="checkbox" v-model="newItem.is_active" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-actions">
|
||||||
|
<button class="btn-primary btn-save" :disabled="isSaving" @click="saveCreate">
|
||||||
|
<span v-if="!isSaving">{{ $t('common.save') }}</span>
|
||||||
|
<span v-else>{{ $t('common.saving') }}</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn-cancel" :disabled="isSaving" @click="cancelCreate">
|
||||||
|
{{ $t('common.cancel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<template v-for="gs in filteredSystems" :key="gs.id">
|
<template v-for="gs in filteredSystems" :key="gs.id">
|
||||||
<tr v-if="editingId !== gs.id">
|
<tr v-if="editingId !== gs.id">
|
||||||
<td>{{ gs.id }}</td>
|
<td>{{ gs.id }}</td>
|
||||||
@@ -33,7 +67,7 @@
|
|||||||
<td>{{ gs.name }}</td>
|
<td>{{ gs.name }}</td>
|
||||||
<td>{{ gs.description || '-' }}</td>
|
<td>{{ gs.description || '-' }}</td>
|
||||||
<td><input type="checkbox" :checked="gs.is_active" disabled /></td>
|
<td><input type="checkbox" :checked="gs.is_active" disabled /></td>
|
||||||
<td><button @click="startEdit(gs)">{{ $t('gamesystem.edit') }}</button></td>
|
<td><button @click="startEdit(gs)">{{ $t('common.edit') }}</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else>
|
<tr v-else>
|
||||||
<td>{{ gs.id }}</td>
|
<td>{{ gs.id }}</td>
|
||||||
@@ -53,12 +87,12 @@
|
|||||||
<input type="checkbox" v-model="editedItem.is_active" />
|
<input type="checkbox" v-model="editedItem.is_active" />
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-actions">
|
<div class="edit-actions">
|
||||||
<button class="btn-primary" :disabled="isSaving" @click="saveEdit">
|
<button class="btn-primary btn-save" :disabled="isSaving" @click="saveEdit">
|
||||||
<span v-if="!isSaving">{{ $t('gamesystem.save') }}</span>
|
<span v-if="!isSaving">{{ $t('common.save') }}</span>
|
||||||
<span v-else>{{ $t('gamesystem.saving') }}</span>
|
<span v-else>{{ $t('common.saving') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
|
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
|
||||||
{{ $t('gamesystem.cancel') }}
|
{{ $t('common.cancel') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,6 +141,8 @@ export default {
|
|||||||
systems: [],
|
systems: [],
|
||||||
editingId: null,
|
editingId: null,
|
||||||
editedItem: null,
|
editedItem: null,
|
||||||
|
creatingNew: false,
|
||||||
|
newItem: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
error: '',
|
error: '',
|
||||||
@@ -149,6 +185,20 @@ export default {
|
|||||||
this.editingId = null
|
this.editingId = null
|
||||||
this.editedItem = null
|
this.editedItem = null
|
||||||
},
|
},
|
||||||
|
startCreate() {
|
||||||
|
this.cancelEdit()
|
||||||
|
this.newItem = {
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
is_active: true,
|
||||||
|
}
|
||||||
|
this.creatingNew = true
|
||||||
|
},
|
||||||
|
cancelCreate() {
|
||||||
|
this.creatingNew = false
|
||||||
|
this.newItem = null
|
||||||
|
},
|
||||||
async saveEdit() {
|
async saveEdit() {
|
||||||
if (!this.editedItem) return
|
if (!this.editedItem) return
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -169,6 +219,32 @@ export default {
|
|||||||
this.isSaving = false
|
this.isSaving = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async saveCreate() {
|
||||||
|
if (!this.newItem) return
|
||||||
|
const code = (this.newItem.code || '').trim()
|
||||||
|
const name = (this.newItem.name || '').trim()
|
||||||
|
if (!code || !name) {
|
||||||
|
alert(this.$t('gamesystem.code') + ' / ' + this.$t('gamesystem.name'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
description: this.newItem.description || '',
|
||||||
|
is_active: !!this.newItem.is_active,
|
||||||
|
}
|
||||||
|
this.isSaving = true
|
||||||
|
try {
|
||||||
|
const resp = await API.post('/api/maintenance/game-systems', payload)
|
||||||
|
this.systems.push(resp.data)
|
||||||
|
this.cancelCreate()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to create game system:', err)
|
||||||
|
this.error = err.response?.data?.error || err.message
|
||||||
|
} finally {
|
||||||
|
this.isSaving = false
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn-primary" @click="startCreate">{{ $t('newEntry') }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="error-box">{{ error }}</div>
|
<div v-if="error" class="error-box">{{ error }}</div>
|
||||||
@@ -38,6 +39,60 @@
|
|||||||
<td colspan="10">{{ $t('common.loading') }}</td>
|
<td colspan="10">{{ $t('common.loading') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr v-if="creatingNew">
|
||||||
|
<td>New</td>
|
||||||
|
<td colspan="9">
|
||||||
|
<div class="edit-form">
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('litsource.code') }}</label>
|
||||||
|
<input v-model="newItem.code" />
|
||||||
|
<label class="inline-label">{{ $t('litsource.name') }}</label>
|
||||||
|
<input v-model="newItem.name" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('litsource.fullName') }}</label>
|
||||||
|
<input v-model="newItem.full_name" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('litsource.edition') }}</label>
|
||||||
|
<input v-model="newItem.edition" />
|
||||||
|
<label class="inline-label">{{ $t('litsource.publisher') }}</label>
|
||||||
|
<input v-model="newItem.publisher" />
|
||||||
|
<label class="inline-label">{{ $t('litsource.year') }}</label>
|
||||||
|
<input v-model.number="newItem.publish_year" type="number" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('litsource.description') }}</label>
|
||||||
|
<input v-model="newItem.description" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('litsource.active') }}</label>
|
||||||
|
<input type="checkbox" v-model="newItem.is_active" />
|
||||||
|
<label class="inline-label">{{ $t('litsource.core') }}</label>
|
||||||
|
<input type="checkbox" v-model="newItem.is_core" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('gamesystem.title') }}</label>
|
||||||
|
<select v-model.number="createSelectedSystemId">
|
||||||
|
<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 btn-save" :disabled="isSaving" @click="saveCreate">
|
||||||
|
<span v-if="!isSaving">{{ $t('common.save') }}</span>
|
||||||
|
<span v-else>{{ $t('common.saving') }}</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn-cancel" :disabled="isSaving" @click="cancelCreate">
|
||||||
|
{{ $t('common.cancel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<template v-for="src in filteredSources" :key="src.id">
|
<template v-for="src in filteredSources" :key="src.id">
|
||||||
<tr v-if="editingId !== src.id">
|
<tr v-if="editingId !== src.id">
|
||||||
<td>{{ src.id }}</td>
|
<td>{{ src.id }}</td>
|
||||||
@@ -49,7 +104,7 @@
|
|||||||
<td>{{ src.publish_year }}</td>
|
<td>{{ src.publish_year }}</td>
|
||||||
<td><input type="checkbox" :checked="src.is_active" disabled /></td>
|
<td><input type="checkbox" :checked="src.is_active" disabled /></td>
|
||||||
<td><input type="checkbox" :checked="src.is_core" disabled /></td>
|
<td><input type="checkbox" :checked="src.is_core" disabled /></td>
|
||||||
<td><button @click="startEdit(src)">{{ $t('litsource.edit') }}</button></td>
|
<td><button @click="startEdit(src)">{{ $t('common.edit') }}</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else>
|
<tr v-else>
|
||||||
<td>{{ src.id }}</td>
|
<td>{{ src.id }}</td>
|
||||||
@@ -92,12 +147,12 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-actions">
|
<div class="edit-actions">
|
||||||
<button class="btn-primary" :disabled="isSaving" @click="saveEdit">
|
<button class="btn-primary btn-save" :disabled="isSaving" @click="saveEdit">
|
||||||
<span v-if="!isSaving">{{ $t('litsource.save') }}</span>
|
<span v-if="!isSaving">{{ $t('common.save') }}</span>
|
||||||
<span v-else>{{ $t('litsource.saving') }}</span>
|
<span v-else>{{ $t('common.saving') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
|
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
|
||||||
{{ $t('litsource.cancel') }}
|
{{ $t('common.cancel') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,6 +214,9 @@ export default {
|
|||||||
sources: [],
|
sources: [],
|
||||||
editingId: null,
|
editingId: null,
|
||||||
editedItem: null,
|
editedItem: null,
|
||||||
|
creatingNew: false,
|
||||||
|
newItem: null,
|
||||||
|
createSelectedSystemId: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
error: '',
|
error: '',
|
||||||
@@ -237,6 +295,28 @@ export default {
|
|||||||
this.loadSources()
|
this.loadSources()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
startCreate() {
|
||||||
|
this.cancelEdit()
|
||||||
|
const defaultSystem = this.currentGameSystem || this.gameSystems.find(s => s.is_active) || this.gameSystems[0] || null
|
||||||
|
this.createSelectedSystemId = defaultSystem ? defaultSystem.id : null
|
||||||
|
this.newItem = {
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
full_name: '',
|
||||||
|
edition: '',
|
||||||
|
publisher: '',
|
||||||
|
publish_year: 0,
|
||||||
|
description: '',
|
||||||
|
is_active: true,
|
||||||
|
is_core: false,
|
||||||
|
}
|
||||||
|
this.creatingNew = true
|
||||||
|
},
|
||||||
|
cancelCreate() {
|
||||||
|
this.creatingNew = false
|
||||||
|
this.newItem = null
|
||||||
|
this.createSelectedSystemId = this.currentGameSystem ? this.currentGameSystem.id : null
|
||||||
|
},
|
||||||
findSystemById(id) {
|
findSystemById(id) {
|
||||||
return findSystemById(this.gameSystems, id)
|
return findSystemById(this.gameSystems, id)
|
||||||
},
|
},
|
||||||
@@ -272,6 +352,35 @@ export default {
|
|||||||
this.isSaving = false
|
this.isSaving = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async saveCreate() {
|
||||||
|
if (!this.newItem) return
|
||||||
|
const targetSystem = this.findSystemById(this.createSelectedSystemId) || this.currentGameSystem
|
||||||
|
const payload = {
|
||||||
|
code: this.newItem.code || '',
|
||||||
|
name: this.newItem.name || '',
|
||||||
|
full_name: this.newItem.full_name || '',
|
||||||
|
edition: this.newItem.edition || '',
|
||||||
|
publisher: this.newItem.publisher || '',
|
||||||
|
publish_year: this.newItem.publish_year || 0,
|
||||||
|
description: this.newItem.description || '',
|
||||||
|
is_active: !!this.newItem.is_active,
|
||||||
|
is_core: !!this.newItem.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.post('/api/maintenance/gsm-lit-sources', payload, { params })
|
||||||
|
this.sources.push(resp.data)
|
||||||
|
this.cancelCreate()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to create source:', err)
|
||||||
|
this.error = err.response?.data?.error || err.message
|
||||||
|
} finally {
|
||||||
|
this.isSaving = false
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
<h2>{{ $t('maintenance') }} - {{ $t('misc.title') }}</h2>
|
<h2>{{ $t('maintenance') }} - {{ $t('misc.title') }}</h2>
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<input v-model="searchTerm" type="text" :placeholder="$t('search')" />
|
<input v-model="searchTerm" type="text" :placeholder="$t('search')" />
|
||||||
|
<select v-model="filterKey">
|
||||||
|
<option value="">{{ $t('all') || 'All' }}</option>
|
||||||
|
<option v-for="key in keyOptions" :key="key" :value="key">{{ key }}</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn-primary" @click="startCreate">{{ $t('newEntry') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -27,6 +32,49 @@
|
|||||||
<td colspan="7">{{ $t('common.loading') }}</td>
|
<td colspan="7">{{ $t('common.loading') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr v-if="creatingNew">
|
||||||
|
<td>New</td>
|
||||||
|
<td colspan="6">
|
||||||
|
<div class="edit-form">
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('misc.key') }}</label>
|
||||||
|
<input v-model="newItem.key" list="misc-key-options" />
|
||||||
|
<datalist id="misc-key-options">
|
||||||
|
<option v-for="key in keyOptions" :key="key" :value="key" />
|
||||||
|
</datalist>
|
||||||
|
<label class="inline-label">{{ $t('misc.value') }}</label>
|
||||||
|
<input v-model="newItem.value" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('misc.source') }}</label>
|
||||||
|
<select v-model.number="newItem.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="newItem.page_number" type="number" min="0" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('misc.system') }}</label>
|
||||||
|
<select v-model.number="createSelectedSystemId">
|
||||||
|
<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 btn-save" :disabled="isSaving" @click="saveCreate">
|
||||||
|
<span v-if="!isSaving">{{ $t('common.save') }}</span>
|
||||||
|
<span v-else>{{ $t('common.saving') }}</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn-cancel" :disabled="isSaving" @click="cancelCreate">
|
||||||
|
{{ $t('common.cancel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<template v-for="item in filteredItems" :key="item.id">
|
<template v-for="item in filteredItems" :key="item.id">
|
||||||
<tr v-if="editingId !== item.id">
|
<tr v-if="editingId !== item.id">
|
||||||
<td>{{ item.id }}</td>
|
<td>{{ item.id }}</td>
|
||||||
@@ -35,7 +83,7 @@
|
|||||||
<td>{{ sourceCodeFor(item.source_id) }}</td>
|
<td>{{ sourceCodeFor(item.source_id) }}</td>
|
||||||
<td>{{ item.page_number || '-' }}</td>
|
<td>{{ item.page_number || '-' }}</td>
|
||||||
<td>{{ systemCodeFor(item.game_system_id, item.game_system) || '-' }}</td>
|
<td>{{ systemCodeFor(item.game_system_id, item.game_system) || '-' }}</td>
|
||||||
<td><button @click="startEdit(item)">{{ $t('misc.edit') }}</button></td>
|
<td><button @click="startEdit(item)">{{ $t('common.edit') }}</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else>
|
<tr v-else>
|
||||||
<td>{{ item.id }}</td>
|
<td>{{ item.id }}</td>
|
||||||
@@ -68,12 +116,12 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-actions">
|
<div class="edit-actions">
|
||||||
<button class="btn-primary" :disabled="isSaving" @click="saveEdit">
|
<button class="btn-primary btn-save" :disabled="isSaving" @click="saveEdit">
|
||||||
<span v-if="!isSaving">{{ $t('misc.save') }}</span>
|
<span v-if="!isSaving">{{ $t('common.save') }}</span>
|
||||||
<span v-else>{{ $t('misc.saving') }}</span>
|
<span v-else>{{ $t('common.saving') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
|
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
|
||||||
{{ $t('misc.cancel') }}
|
{{ $t('common.cancel') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,10 +184,14 @@ export default {
|
|||||||
editingId: null,
|
editingId: null,
|
||||||
editedItem: null,
|
editedItem: null,
|
||||||
selectedSystemId: null,
|
selectedSystemId: null,
|
||||||
|
creatingNew: false,
|
||||||
|
newItem: null,
|
||||||
|
createSelectedSystemId: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
error: '',
|
error: '',
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
filterKey: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
@@ -148,12 +200,17 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
filteredItems() {
|
filteredItems() {
|
||||||
const term = this.searchTerm.trim().toLowerCase()
|
const term = this.searchTerm.trim().toLowerCase()
|
||||||
const list = term
|
let list = term
|
||||||
? this.items.filter(it =>
|
? this.items.filter(it =>
|
||||||
(it.key || '').toLowerCase().includes(term) ||
|
(it.key || '').toLowerCase().includes(term) ||
|
||||||
(it.value || '').toLowerCase().includes(term)
|
(it.value || '').toLowerCase().includes(term)
|
||||||
)
|
)
|
||||||
: this.items
|
: this.items
|
||||||
|
|
||||||
|
if (this.filterKey) {
|
||||||
|
list = list.filter(it => it.key === this.filterKey)
|
||||||
|
}
|
||||||
|
|
||||||
return [...list].sort((a, b) => (a.key || '').localeCompare(b.key || ''))
|
return [...list].sort((a, b) => (a.key || '').localeCompare(b.key || ''))
|
||||||
},
|
},
|
||||||
keyOptions() {
|
keyOptions() {
|
||||||
@@ -199,6 +256,23 @@ export default {
|
|||||||
await this.loadSources()
|
await this.loadSources()
|
||||||
await this.loadItems()
|
await this.loadItems()
|
||||||
},
|
},
|
||||||
|
startCreate() {
|
||||||
|
this.cancelEdit()
|
||||||
|
const defaultSystem = this.currentGameSystem || this.gameSystems.find(gs => gs.is_active) || this.gameSystems[0] || null
|
||||||
|
this.createSelectedSystemId = defaultSystem ? defaultSystem.id : null
|
||||||
|
this.newItem = {
|
||||||
|
key: '',
|
||||||
|
value: '',
|
||||||
|
source_id: null,
|
||||||
|
page_number: 0,
|
||||||
|
}
|
||||||
|
this.creatingNew = true
|
||||||
|
},
|
||||||
|
cancelCreate() {
|
||||||
|
this.creatingNew = false
|
||||||
|
this.newItem = null
|
||||||
|
this.createSelectedSystemId = null
|
||||||
|
},
|
||||||
async loadGameSystems() {
|
async loadGameSystems() {
|
||||||
try {
|
try {
|
||||||
const systems = await fetchGameSystems()
|
const systems = await fetchGameSystems()
|
||||||
@@ -279,6 +353,27 @@ export default {
|
|||||||
this.isSaving = false
|
this.isSaving = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async saveCreate() {
|
||||||
|
if (!this.newItem) return
|
||||||
|
const payload = {
|
||||||
|
key: this.newItem.key || '',
|
||||||
|
value: this.newItem.value || '',
|
||||||
|
source_id: this.newItem.source_id || null,
|
||||||
|
page_number: this.newItem.page_number || 0,
|
||||||
|
}
|
||||||
|
this.isSaving = true
|
||||||
|
try {
|
||||||
|
const params = this.buildParamsForSystemId(this.createSelectedSystemId)
|
||||||
|
const resp = await API.post('/api/maintenance/gsm-misc', payload, { params })
|
||||||
|
this.items.push(resp.data)
|
||||||
|
this.cancelCreate()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to create misc entry:', err)
|
||||||
|
this.error = err.response?.data?.error || err.message
|
||||||
|
} finally {
|
||||||
|
this.isSaving = false
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
<h2>{{ $t('maintenance') }} - {{ $t('skillimprovement.title') }}</h2>
|
<h2>{{ $t('maintenance') }} - {{ $t('skillimprovement.title') }}</h2>
|
||||||
|
<button class="btn-primary" @click="startCreate">{{ $t('newEntry') }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="error-box">{{ error }}</div>
|
<div v-if="error" class="error-box">{{ error }}</div>
|
||||||
@@ -22,6 +23,42 @@
|
|||||||
<tr v-if="isLoading">
|
<tr v-if="isLoading">
|
||||||
<td colspan="6">{{ $t('common.loading') }}</td>
|
<td colspan="6">{{ $t('common.loading') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr v-if="creatingNew">
|
||||||
|
<td>New</td>
|
||||||
|
<td colspan="5">
|
||||||
|
<div class="edit-form">
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('skillimprovement.level') }}</label>
|
||||||
|
<input v-model.number="newItem.current_level" type="number" />
|
||||||
|
<label class="inline-label">{{ $t('skillimprovement.te') }}</label>
|
||||||
|
<input v-model.number="newItem.te_required" type="number" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-row">
|
||||||
|
<label>{{ $t('skillimprovement.category') }}</label>
|
||||||
|
<select v-model.number="newItem.category_id">
|
||||||
|
<option v-for="cat in categoryOptions" :key="cat.id" :value="cat.id">
|
||||||
|
{{ cat.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<label class="inline-label">{{ $t('skillimprovement.difficulty') }}</label>
|
||||||
|
<select v-model.number="newItem.difficulty_id">
|
||||||
|
<option v-for="diff in difficultyOptions" :key="diff.id" :value="diff.id">
|
||||||
|
{{ diff.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="edit-actions">
|
||||||
|
<button class="btn-primary btn-save" :disabled="isSaving" @click="saveCreate">
|
||||||
|
<span v-if="!isSaving">{{ $t('common.save') }}</span>
|
||||||
|
<span v-else>{{ $t('common.saving') }}</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn-cancel" :disabled="isSaving" @click="cancelCreate">
|
||||||
|
{{ $t('common.cancel') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<template v-for="cost in costs" :key="cost.id">
|
<template v-for="cost in costs" :key="cost.id">
|
||||||
<tr v-if="editingId !== cost.id">
|
<tr v-if="editingId !== cost.id">
|
||||||
<td>{{ cost.id }}</td>
|
<td>{{ cost.id }}</td>
|
||||||
@@ -29,7 +66,7 @@
|
|||||||
<td>{{ cost.te_required }}</td>
|
<td>{{ cost.te_required }}</td>
|
||||||
<td>{{ displayCategory(cost) }}</td>
|
<td>{{ displayCategory(cost) }}</td>
|
||||||
<td>{{ displayDifficulty(cost) }}</td>
|
<td>{{ displayDifficulty(cost) }}</td>
|
||||||
<td><button @click="startEdit(cost)">{{ $t('skillimprovement.edit') }}</button></td>
|
<td><button @click="startEdit(cost)">{{ $t('common.edit') }}</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else>
|
<tr v-else>
|
||||||
<td>{{ cost.id }}</td>
|
<td>{{ cost.id }}</td>
|
||||||
@@ -56,12 +93,12 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-actions">
|
<div class="edit-actions">
|
||||||
<button class="btn-primary" :disabled="isSaving" @click="saveEdit">
|
<button class="btn-primary btn-save" :disabled="isSaving" @click="saveEdit">
|
||||||
<span v-if="!isSaving">{{ $t('skillimprovement.save') }}</span>
|
<span v-if="!isSaving">{{ $t('common.save') }}</span>
|
||||||
<span v-else>{{ $t('skillimprovement.saving') }}</span>
|
<span v-else>{{ $t('common.saving') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
|
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
|
||||||
{{ $t('skillimprovement.cancel') }}
|
{{ $t('common.cancel') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,6 +150,8 @@ export default {
|
|||||||
costs: [],
|
costs: [],
|
||||||
editingId: null,
|
editingId: null,
|
||||||
editedItem: null,
|
editedItem: null,
|
||||||
|
creatingNew: false,
|
||||||
|
newItem: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
error: '',
|
error: '',
|
||||||
@@ -177,6 +216,22 @@ export default {
|
|||||||
this.editingId = null
|
this.editingId = null
|
||||||
this.editedItem = null
|
this.editedItem = null
|
||||||
},
|
},
|
||||||
|
startCreate() {
|
||||||
|
this.cancelEdit()
|
||||||
|
const defaultCategory = this.categoryOptions[0]?.id ?? null
|
||||||
|
const defaultDifficulty = this.difficultyOptions[0]?.id ?? null
|
||||||
|
this.newItem = {
|
||||||
|
current_level: 0,
|
||||||
|
te_required: 0,
|
||||||
|
category_id: defaultCategory,
|
||||||
|
difficulty_id: defaultDifficulty,
|
||||||
|
}
|
||||||
|
this.creatingNew = true
|
||||||
|
},
|
||||||
|
cancelCreate() {
|
||||||
|
this.creatingNew = false
|
||||||
|
this.newItem = null
|
||||||
|
},
|
||||||
async saveEdit() {
|
async saveEdit() {
|
||||||
if (!this.editedItem) return
|
if (!this.editedItem) return
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -198,6 +253,26 @@ export default {
|
|||||||
this.isSaving = false
|
this.isSaving = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async saveCreate() {
|
||||||
|
if (!this.newItem) return
|
||||||
|
const payload = {
|
||||||
|
current_level: this.newItem.current_level,
|
||||||
|
te_required: this.newItem.te_required,
|
||||||
|
category_id: this.newItem.category_id,
|
||||||
|
difficulty_id: this.newItem.difficulty_id,
|
||||||
|
}
|
||||||
|
this.isSaving = true
|
||||||
|
try {
|
||||||
|
const resp = await API.post('/api/maintenance/skill-improvement-cost2', payload)
|
||||||
|
this.costs.push(resp.data)
|
||||||
|
this.cancelCreate()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to create cost:', err)
|
||||||
|
this.error = err.response?.data?.error || err.message
|
||||||
|
} finally {
|
||||||
|
this.isSaving = false
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -209,8 +209,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="edit-actions">
|
<div class="edit-actions">
|
||||||
<button @click="saveCreate" class="btn-save">{{ $t('createSkill') }}</button>
|
<button @click="saveCreate" class="btn-save">{{ $t('common.save') }}</button>
|
||||||
<button @click="cancelCreate" class="btn-cancel">Cancel</button>
|
<button @click="cancelCreate" class="btn-cancel">{{ $t('common.cancel') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -232,7 +232,7 @@
|
|||||||
<td>{{ formatQuelle(dtaItem) }}</td>
|
<td>{{ formatQuelle(dtaItem) }}</td>
|
||||||
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.game_system || 'midgard') }}</td>
|
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.game_system || 'midgard') }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button @click="startEdit(index)">Edit</button>
|
<button @click="startEdit(index)">{{ $t('common.edit') }}</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -340,8 +340,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="edit-actions">
|
<div class="edit-actions">
|
||||||
<button @click="saveEdit(index)" class="btn-save">Save</button>
|
<button @click="saveEdit(index)" class="btn-save">{{ $t('common.save') }}</button>
|
||||||
<button @click="cancelEdit" class="btn-cancel">Cancel</button>
|
<button @click="cancelEdit" class="btn-cancel">{{ $t('common.cancel') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
v-model="searchTerm"
|
v-model="searchTerm"
|
||||||
:placeholder="`${$t('search')} ${$t('Spell')}...`"
|
:placeholder="`${$t('search')} ${$t('Spell')}...`"
|
||||||
/>
|
/>
|
||||||
|
<button @click="startCreate" class="btn-primary">{{ $t('newEntry') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -39,6 +40,32 @@
|
|||||||
{{ $t('spell.import') || 'Import Spells' }}
|
{{ $t('spell.import') || 'Import Spells' }}
|
||||||
</button>
|
</button>
|
||||||
<div v-if="importResult" class="import-result" :class="importResult.success ? 'success' : 'error'">
|
<div v-if="importResult" class="import-result" :class="importResult.success ? 'success' : 'error'">
|
||||||
|
async saveCreate() {
|
||||||
|
if (!this.newItem) return
|
||||||
|
try {
|
||||||
|
const source = this.availableSources.find(s => s.code === this.newItem.sourceCode)
|
||||||
|
const selectedSystem = this.gameSystems.find(gs => gs.id === this.createSelectedSystemId)
|
||||||
|
|
||||||
|
const createData = {
|
||||||
|
...this.newItem,
|
||||||
|
source_id: source ? source.id : null,
|
||||||
|
page_number: this.newItem.page_number || 0,
|
||||||
|
system: selectedSystem ? selectedSystem.code : (this.newItem.system || ''),
|
||||||
|
game_system_id: selectedSystem ? selectedSystem.id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await API.post(
|
||||||
|
'/api/maintenance/spells-enhanced',
|
||||||
|
createData
|
||||||
|
)
|
||||||
|
|
||||||
|
this.enhancedSpells.push(response.data)
|
||||||
|
this.cancelCreate()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create spell:', error)
|
||||||
|
alert('Failed to create spell: ' + (error.response?.data?.error || error.message))
|
||||||
|
}
|
||||||
|
},
|
||||||
{{ importResult.message }}
|
{{ importResult.message }}
|
||||||
<span v-if="importResult.total_spells"> ({{ importResult.total_spells }} spells total)</span>
|
<span v-if="importResult.total_spells"> ({{ importResult.total_spells }} spells total)</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,6 +148,102 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr v-if="creatingNew">
|
||||||
|
<td>New</td>
|
||||||
|
<td colspan="14">
|
||||||
|
<div class="edit-form">
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('spell.name') }}:</label>
|
||||||
|
<input v-model="newItem.name" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('spell.category') }}:</label>
|
||||||
|
<select v-model="newItem.category" style="width:120px;">
|
||||||
|
<option v-for="category in mdata['spellcategories']" :key="category" :value="category">
|
||||||
|
{{ category }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('spell.level') }}:</label>
|
||||||
|
<input v-model.number="newItem.level" type="number" style="width:60px;" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('spell.apverbrauch') }}:</label>
|
||||||
|
<input v-model="newItem.ap" style="width:60px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('spell.zauberdauer') }}:</label>
|
||||||
|
<input v-model="newItem.zauberdauer" style="width:120px;" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('spell.reichweite') }}:</label>
|
||||||
|
<input v-model="newItem.reichweite" style="width:120px;" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('spell.wirkungsdauer') }}:</label>
|
||||||
|
<input v-model="newItem.wirkungsdauer" style="width:120px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('spell.wirkungsziel') }}:</label>
|
||||||
|
<input v-model="newItem.wirkungsziel" style="width:150px;" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('spell.wirkungsbereich') }}:</label>
|
||||||
|
<input v-model="newItem.wirkungsbereich" style="width:150px;" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('spell.ursprung') }}:</label>
|
||||||
|
<input v-model="newItem.ursprung" style="width:120px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field full-width">
|
||||||
|
<label>{{ $t('spell.description') }}:</label>
|
||||||
|
<input v-model="newItem.beschreibung" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('spell.quelle') }}:</label>
|
||||||
|
<select v-model="newItem.sourceCode" style="width:100px;">
|
||||||
|
<option value="">-</option>
|
||||||
|
<option v-for="source in availableSources" :key="source.code" :value="source.code">
|
||||||
|
{{ source.code }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('spell.page') || 'Page' }}:</label>
|
||||||
|
<input v-model.number="newItem.page_number" type="number" style="width:60px;" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('spell.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-actions">
|
||||||
|
<button @click="saveCreate" class="btn-save">{{ $t('common.save') }}</button>
|
||||||
|
<button @click="cancelCreate" class="btn-cancel">{{ $t('common.cancel') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<template v-for="(dtaItem, index) in filteredAndSortedSpells" :key="dtaItem.id">
|
<template v-for="(dtaItem, index) in filteredAndSortedSpells" :key="dtaItem.id">
|
||||||
<tr v-if="editingIndex !== index">
|
<tr v-if="editingIndex !== index">
|
||||||
<td>{{ dtaItem.id || '' }}</td>
|
<td>{{ dtaItem.id || '' }}</td>
|
||||||
@@ -138,7 +261,7 @@
|
|||||||
<td>{{ formatQuelle(dtaItem) }}</td>
|
<td>{{ formatQuelle(dtaItem) }}</td>
|
||||||
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.system || 'midgard') }}</td>
|
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.system || 'midgard') }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button @click="startEdit(index)">Edit</button>
|
<button @click="startEdit(index)">{{ $t('common.edit') }}</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Edit Mode -->
|
<!-- Edit Mode -->
|
||||||
@@ -233,8 +356,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="edit-actions">
|
<div class="edit-actions">
|
||||||
<button @click="saveEdit(index)" class="btn-save">Save</button>
|
<button @click="saveEdit(index)" class="btn-save">{{ $t('common.save') }}</button>
|
||||||
<button @click="cancelEdit" class="btn-cancel">Cancel</button>
|
<button @click="cancelEdit" class="btn-cancel">{{ $t('common.cancel') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -397,7 +520,10 @@ export default {
|
|||||||
enhancedSpells: [],
|
enhancedSpells: [],
|
||||||
availableSources: [],
|
availableSources: [],
|
||||||
gameSystems: [],
|
gameSystems: [],
|
||||||
selectedSystemId: null
|
selectedSystemId: null,
|
||||||
|
creatingNew: false,
|
||||||
|
newItem: null,
|
||||||
|
createSelectedSystemId: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
@@ -592,6 +718,33 @@ export default {
|
|||||||
this.editedItem = null;
|
this.editedItem = null;
|
||||||
this.selectedSystemId = null;
|
this.selectedSystemId = null;
|
||||||
},
|
},
|
||||||
|
startCreate() {
|
||||||
|
this.cancelEdit()
|
||||||
|
const defaultSystem = this.gameSystems.find(gs => gs.is_active) || this.gameSystems[0] || null
|
||||||
|
this.createSelectedSystemId = defaultSystem ? defaultSystem.id : null
|
||||||
|
this.newItem = {
|
||||||
|
name: '',
|
||||||
|
category: this.mdata.spellcategories?.[0] || '',
|
||||||
|
level: 0,
|
||||||
|
ap: '',
|
||||||
|
zauberdauer: '',
|
||||||
|
reichweite: '',
|
||||||
|
wirkungsziel: '',
|
||||||
|
wirkungsbereich: '',
|
||||||
|
wirkungsdauer: '',
|
||||||
|
ursprung: '',
|
||||||
|
beschreibung: '',
|
||||||
|
sourceCode: '',
|
||||||
|
page_number: 0,
|
||||||
|
system: defaultSystem ? defaultSystem.code : ''
|
||||||
|
}
|
||||||
|
this.creatingNew = true
|
||||||
|
},
|
||||||
|
cancelCreate() {
|
||||||
|
this.creatingNew = false
|
||||||
|
this.newItem = null
|
||||||
|
this.createSelectedSystemId = null
|
||||||
|
},
|
||||||
findSystemIdByCode(code) {
|
findSystemIdByCode(code) {
|
||||||
return findSystemIdByCode(this.gameSystems, code)
|
return findSystemIdByCode(this.gameSystems, code)
|
||||||
},
|
},
|
||||||
@@ -605,25 +758,51 @@ export default {
|
|||||||
},
|
},
|
||||||
async handleSpellUpdate({ index, spell }) {
|
async handleSpellUpdate({ index, spell }) {
|
||||||
try {
|
try {
|
||||||
const response = await API.put(
|
const response = await API.put(
|
||||||
`/api/maintenance/spells/${spell.id}`, spell,
|
`/api/maintenance/spells/${spell.id}`, spell,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem('token')}` ,
|
Authorization: `Bearer ${localStorage.getItem('token')}` ,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
if (!response.statusText== "OK") throw new Error('Update failed');
|
)
|
||||||
const updatedSkill = response.data;
|
if (!response.statusText== "OK") throw new Error('Update failed');
|
||||||
// Update the spell in mdata
|
const updatedSkill = response.data;
|
||||||
this.mdata.spells = this.mdata.spells.map(s =>
|
// Update the spell in mdata
|
||||||
s.id === updatedSkill.id ? updatedSkill : s
|
this.mdata.spells = this.mdata.spells.map(s =>
|
||||||
);
|
s.id === updatedSkill.id ? updatedSkill : s
|
||||||
} catch (error) {
|
);
|
||||||
console.error('Failed to update spell:', error);
|
} catch (error) {
|
||||||
|
console.error('Failed to update spell:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async saveCreate() {
|
||||||
|
if (!this.newItem) return
|
||||||
|
try {
|
||||||
|
const source = this.availableSources.find(s => s.code === this.newItem.sourceCode)
|
||||||
|
const selectedSystem = this.gameSystems.find(gs => gs.id === this.createSelectedSystemId)
|
||||||
|
|
||||||
|
const createData = {
|
||||||
|
...this.newItem,
|
||||||
|
source_id: source ? source.id : null,
|
||||||
|
page_number: this.newItem.page_number || 0,
|
||||||
|
system: selectedSystem ? selectedSystem.code : (this.newItem.system || ''),
|
||||||
|
game_system_id: selectedSystem ? selectedSystem.id : null
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
const response = await API.post(
|
||||||
|
'/api/maintenance/spells-enhanced',
|
||||||
|
createData
|
||||||
|
)
|
||||||
|
|
||||||
|
this.enhancedSpells.push(response.data)
|
||||||
|
this.cancelCreate()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create spell:', error)
|
||||||
|
alert('Failed to create spell: ' + (error.response?.data?.error || error.message))
|
||||||
|
}
|
||||||
|
},
|
||||||
handleFileSelect(event) {
|
handleFileSelect(event) {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (file && file.type === 'text/csv') {
|
if (file && file.type === 'text/csv') {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
v-model="searchTerm"
|
v-model="searchTerm"
|
||||||
:placeholder="`${$t('search')} ${$t('WaeponSkill')}...`"
|
:placeholder="`${$t('search')} ${$t('WaeponSkill')}...`"
|
||||||
/>
|
/>
|
||||||
|
<button @click="startCreate" class="btn-primary">{{ $t('newEntry') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -47,6 +48,69 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr v-if="creatingNew">
|
||||||
|
<td>New</td>
|
||||||
|
<td colspan="6">
|
||||||
|
<div class="edit-form">
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weaponskill.name') }}:</label>
|
||||||
|
<input v-model="newItem.name" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weaponskill.difficulty') }}:</label>
|
||||||
|
<select v-model="newItem.difficulty" style="width:120px;">
|
||||||
|
<option value="leicht">leicht</option>
|
||||||
|
<option value="normal">normal</option>
|
||||||
|
<option value="schwer">schwer</option>
|
||||||
|
<option value="sehr schwer">sehr schwer</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weaponskill.initialwert') }}:</label>
|
||||||
|
<input v-model.number="newItem.initialwert" type="number" style="width:60px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field full-width">
|
||||||
|
<label>{{ $t('weaponskill.description') }}:</label>
|
||||||
|
<input v-model="newItem.beschreibung" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weaponskill.quelle') }}:</label>
|
||||||
|
<select v-model="newItem.sourceCode" style="width:100px;">
|
||||||
|
<option value="">-</option>
|
||||||
|
<option v-for="source in availableSources" :key="source.code" :value="source.code">
|
||||||
|
{{ source.code }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weaponskill.page') || 'Page' }}:</label>
|
||||||
|
<input v-model.number="newItem.page_number" type="number" style="width:60px;" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weaponskill.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-actions">
|
||||||
|
<button @click="saveCreate" class="btn-save">{{ $t('common.save') }}</button>
|
||||||
|
<button @click="cancelCreate" class="btn-cancel">{{ $t('common.cancel') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<template v-for="(dtaItem, index) in filteredAndSortedWaeponSkills" :key="dtaItem.id">
|
<template v-for="(dtaItem, index) in filteredAndSortedWaeponSkills" :key="dtaItem.id">
|
||||||
<tr v-if="editingIndex !== index">
|
<tr v-if="editingIndex !== index">
|
||||||
<td>{{ dtaItem.id || '' }}</td>
|
<td>{{ dtaItem.id || '' }}</td>
|
||||||
@@ -57,7 +121,7 @@
|
|||||||
<td>{{ formatQuelle(dtaItem) }}</td>
|
<td>{{ formatQuelle(dtaItem) }}</td>
|
||||||
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.system || 'midgard') }}</td>
|
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.system || 'midgard') }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button @click="startEdit(index)">Edit</button>
|
<button @click="startEdit(index)">{{ $t('common.edit') }}</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Edit Mode -->
|
<!-- Edit Mode -->
|
||||||
@@ -119,8 +183,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="edit-actions">
|
<div class="edit-actions">
|
||||||
<button @click="saveEdit(index)" class="btn-save">Save</button>
|
<button @click="saveEdit(index)" class="btn-save">{{ $t('common.save') }}</button>
|
||||||
<button @click="cancelEdit" class="btn-cancel">Cancel</button>
|
<button @click="cancelEdit" class="btn-cancel">{{ $t('common.cancel') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -211,7 +275,10 @@ export default {
|
|||||||
availableSources: [],
|
availableSources: [],
|
||||||
availableDifficultiesData: [],
|
availableDifficultiesData: [],
|
||||||
gameSystems: [],
|
gameSystems: [],
|
||||||
selectedSystemId: null
|
selectedSystemId: null,
|
||||||
|
creatingNew: false,
|
||||||
|
newItem: null,
|
||||||
|
createSelectedSystemId: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
@@ -345,6 +412,53 @@ export default {
|
|||||||
this.editedItem = null
|
this.editedItem = null
|
||||||
this.selectedSystemId = null
|
this.selectedSystemId = null
|
||||||
},
|
},
|
||||||
|
startCreate() {
|
||||||
|
this.cancelEdit()
|
||||||
|
const defaultSystem = this.gameSystems.find(gs => gs.is_active) || this.gameSystems[0] || null
|
||||||
|
this.createSelectedSystemId = defaultSystem ? defaultSystem.id : null
|
||||||
|
this.newItem = {
|
||||||
|
name: '',
|
||||||
|
difficulty: 'leicht',
|
||||||
|
initialwert: 0,
|
||||||
|
beschreibung: '',
|
||||||
|
sourceCode: '',
|
||||||
|
page_number: 0,
|
||||||
|
system: defaultSystem ? defaultSystem.code : ''
|
||||||
|
}
|
||||||
|
this.creatingNew = true
|
||||||
|
},
|
||||||
|
cancelCreate() {
|
||||||
|
this.creatingNew = false
|
||||||
|
this.newItem = null
|
||||||
|
this.createSelectedSystemId = null
|
||||||
|
},
|
||||||
|
async saveCreate() {
|
||||||
|
if (!this.newItem) return
|
||||||
|
try {
|
||||||
|
const source = this.availableSources.find(s => s.code === this.newItem.sourceCode)
|
||||||
|
const selectedSystem = this.gameSystems.find(gs => gs.id === this.createSelectedSystemId)
|
||||||
|
|
||||||
|
const createData = {
|
||||||
|
...this.newItem,
|
||||||
|
category: 'Waffen',
|
||||||
|
source_id: source ? source.id : null,
|
||||||
|
page_number: this.newItem.page_number || 0,
|
||||||
|
system: selectedSystem ? selectedSystem.code : (this.newItem.system || ''),
|
||||||
|
game_system_id: selectedSystem ? selectedSystem.id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await API.post(
|
||||||
|
'/api/maintenance/weaponskills-enhanced',
|
||||||
|
createData
|
||||||
|
)
|
||||||
|
|
||||||
|
this.enhancedWeaponSkills.push(response.data)
|
||||||
|
this.cancelCreate()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create weapon skill:', error)
|
||||||
|
alert('Failed to create weapon skill: ' + (error.response?.data?.error || error.message))
|
||||||
|
}
|
||||||
|
},
|
||||||
findSystemIdByCode(code) {
|
findSystemIdByCode(code) {
|
||||||
return findSystemIdByCode(this.gameSystems, code)
|
return findSystemIdByCode(this.gameSystems, code)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
v-model="searchTerm"
|
v-model="searchTerm"
|
||||||
:placeholder="`${$t('search')} ${$t('Weapons')}...`"
|
:placeholder="`${$t('search')} ${$t('Weapons')}...`"
|
||||||
/>
|
/>
|
||||||
|
<button @click="startCreate" class="btn-primary">{{ $t('newEntry') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -82,6 +83,110 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr v-if="creatingNew">
|
||||||
|
<td>New</td>
|
||||||
|
<td colspan="11">
|
||||||
|
<div class="edit-form">
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weapon.name') }}:</label>
|
||||||
|
<input v-model="newItem.name" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weapon.skillrequired') || 'Skill Required' }}:</label>
|
||||||
|
<input v-model="newItem.skill_required" style="width:150px;" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weapon.weight') }}:</label>
|
||||||
|
<input v-model.number="newItem.gewicht" type="number" style="width:80px;" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weapon.value') }}:</label>
|
||||||
|
<input v-model="newItem.wert" style="width:100px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weapon.damage') }}:</label>
|
||||||
|
<input v-model="newItem.damage" style="width:100px;" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weapon.rangenear') || 'Range Near' }}:</label>
|
||||||
|
<input v-model.number="newItem.range_near" type="number" style="width:80px;" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weapon.rangemiddle') || 'Range Middle' }}:</label>
|
||||||
|
<input v-model.number="newItem.range_middle" type="number" style="width:80px;" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weapon.rangefar') || 'Range Far' }}:</label>
|
||||||
|
<input v-model.number="newItem.range_far" type="number" style="width:80px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weapon.bonusskill') || 'Bonus' }}:</label>
|
||||||
|
<select v-model="newItem.bonuseigenschaft" style="width:80px;">
|
||||||
|
<option value="">-</option>
|
||||||
|
<option value="St">St</option>
|
||||||
|
<option value="Gs">Gs</option>
|
||||||
|
<option value="Gw">Gw</option>
|
||||||
|
<option value="Ko">Ko</option>
|
||||||
|
<option value="In">In</option>
|
||||||
|
<option value="Zt">Zt</option>
|
||||||
|
<option value="Au">Au</option>
|
||||||
|
<option value="pA">pA</option>
|
||||||
|
<option value="Wk">Wk</option>
|
||||||
|
<option value="B">B</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weapon.personal_item') }}:</label>
|
||||||
|
<input type="checkbox" v-model="newItem.personal_item" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field full-width">
|
||||||
|
<label>{{ $t('weapon.description') }}:</label>
|
||||||
|
<input v-model="newItem.beschreibung" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weapon.quelle') }}:</label>
|
||||||
|
<select v-model="newItem.sourceCode" style="width:100px;">
|
||||||
|
<option value="">-</option>
|
||||||
|
<option v-for="source in availableSources" :key="source.code" :value="source.code">
|
||||||
|
{{ source.code }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weapon.page') || 'Page' }}:</label>
|
||||||
|
<input v-model.number="newItem.page_number" type="number" style="width:60px;" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('weapon.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-actions">
|
||||||
|
<button @click="saveCreate" class="btn-save">{{ $t('common.save') }}</button>
|
||||||
|
<button @click="cancelCreate" class="btn-cancel">{{ $t('common.cancel') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<template v-for="(dtaItem, index) in filteredAndSortedWeaponss" :key="dtaItem.id">
|
<template v-for="(dtaItem, index) in filteredAndSortedWeaponss" :key="dtaItem.id">
|
||||||
<tr v-if="editingIndex !== index">
|
<tr v-if="editingIndex !== index">
|
||||||
<td>{{ dtaItem.id || '' }}</td>
|
<td>{{ dtaItem.id || '' }}</td>
|
||||||
@@ -98,7 +203,7 @@
|
|||||||
<td><input type="checkbox" :checked="dtaItem.personal_item" disabled /></td>
|
<td><input type="checkbox" :checked="dtaItem.personal_item" disabled /></td>
|
||||||
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.system || 'midgard') }}</td>
|
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.system || 'midgard') }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button @click="startEdit(index)">Edit</button>
|
<button @click="startEdit(index)">{{ $t('common.edit') }}</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Edit Mode -->
|
<!-- Edit Mode -->
|
||||||
@@ -201,8 +306,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="edit-actions">
|
<div class="edit-actions">
|
||||||
<button @click="saveEdit(index)" class="btn-save">Save</button>
|
<button @click="saveEdit(index)" class="btn-save">{{ $t('common.save') }}</button>
|
||||||
<button @click="cancelEdit" class="btn-cancel">Cancel</button>
|
<button @click="cancelEdit" class="btn-cancel">{{ $t('common.cancel') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -296,7 +401,10 @@ export default {
|
|||||||
enhancedWeapons: [],
|
enhancedWeapons: [],
|
||||||
availableSources: [],
|
availableSources: [],
|
||||||
gameSystems: [],
|
gameSystems: [],
|
||||||
selectedSystemId: null
|
selectedSystemId: null,
|
||||||
|
creatingNew: false,
|
||||||
|
newItem: null,
|
||||||
|
createSelectedSystemId: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
@@ -478,6 +586,59 @@ export default {
|
|||||||
this.editedItem = null
|
this.editedItem = null
|
||||||
this.selectedSystemId = null
|
this.selectedSystemId = null
|
||||||
},
|
},
|
||||||
|
startCreate() {
|
||||||
|
this.cancelEdit()
|
||||||
|
const defaultSystem = this.gameSystems.find(gs => gs.is_active) || this.gameSystems[0] || null
|
||||||
|
this.createSelectedSystemId = defaultSystem ? defaultSystem.id : null
|
||||||
|
this.newItem = {
|
||||||
|
name: '',
|
||||||
|
skill_required: '',
|
||||||
|
gewicht: 0,
|
||||||
|
wert: '',
|
||||||
|
damage: '',
|
||||||
|
range_near: 0,
|
||||||
|
range_middle: 0,
|
||||||
|
range_far: 0,
|
||||||
|
beschreibung: '',
|
||||||
|
bonuseigenschaft: '',
|
||||||
|
personal_item: false,
|
||||||
|
sourceCode: '',
|
||||||
|
page_number: 0,
|
||||||
|
system: defaultSystem ? defaultSystem.code : ''
|
||||||
|
}
|
||||||
|
this.creatingNew = true
|
||||||
|
},
|
||||||
|
cancelCreate() {
|
||||||
|
this.creatingNew = false
|
||||||
|
this.newItem = null
|
||||||
|
this.createSelectedSystemId = null
|
||||||
|
},
|
||||||
|
async saveCreate() {
|
||||||
|
if (!this.newItem) return
|
||||||
|
try {
|
||||||
|
const source = this.availableSources.find(s => s.code === this.newItem.sourceCode)
|
||||||
|
const selectedSystem = this.gameSystems.find(gs => gs.id === this.createSelectedSystemId)
|
||||||
|
|
||||||
|
const createData = {
|
||||||
|
...this.newItem,
|
||||||
|
source_id: source ? source.id : null,
|
||||||
|
page_number: this.newItem.page_number || 0,
|
||||||
|
system: selectedSystem ? selectedSystem.code : (this.newItem.system || ''),
|
||||||
|
game_system_id: selectedSystem ? selectedSystem.id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await API.post(
|
||||||
|
'/api/maintenance/weapons-enhanced',
|
||||||
|
createData
|
||||||
|
)
|
||||||
|
|
||||||
|
this.enhancedWeapons.push(response.data)
|
||||||
|
this.cancelCreate()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create weapon:', error)
|
||||||
|
alert('Failed to create weapon: ' + (error.response?.data?.error || error.message))
|
||||||
|
}
|
||||||
|
},
|
||||||
findSystemIdByCode(code) {
|
findSystemIdByCode(code) {
|
||||||
return findSystemIdByCode(this.gameSystems, code)
|
return findSystemIdByCode(this.gameSystems, code)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ export default {
|
|||||||
previous: 'Zurück',
|
previous: 'Zurück',
|
||||||
next: 'Weiter',
|
next: 'Weiter',
|
||||||
back: 'Zurück',
|
back: 'Zurück',
|
||||||
|
save: 'Speichern',
|
||||||
|
saving: 'Speichern...',
|
||||||
|
edit: 'Bearbeiten',
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
login: 'Anmelden',
|
login: 'Anmelden',
|
||||||
@@ -21,6 +24,19 @@ export default {
|
|||||||
dontHaveAccount: 'Noch kein Konto?',
|
dontHaveAccount: 'Noch kein Konto?',
|
||||||
registerHere: 'Hier registrieren'
|
registerHere: 'Hier registrieren'
|
||||||
},
|
},
|
||||||
|
forgotPassword: {
|
||||||
|
title: 'Passwort zurücksetzen',
|
||||||
|
description: 'Geben Sie Ihre E-Mail-Adresse ein, um einen Reset-Link zu erhalten.',
|
||||||
|
emailLabel: 'E-Mail-Adresse',
|
||||||
|
emailPlaceholder: 'ihre{\'@\'}email.de',
|
||||||
|
submit: 'Reset-Link senden',
|
||||||
|
submitting: 'Wird gesendet...',
|
||||||
|
successTitle: 'E-Mail gesendet!',
|
||||||
|
successInfo: 'Falls ein Account mit dieser E-Mail-Adresse existiert, wurde ein Reset-Link gesendet.',
|
||||||
|
successHint: 'Prüfen Sie Ihre E-Mails und folgen Sie dem Link.',
|
||||||
|
error: 'Fehler beim Senden der E-Mail. Versuchen Sie es später erneut.',
|
||||||
|
backToLogin: 'Zurück zum Login'
|
||||||
|
},
|
||||||
import: {
|
import: {
|
||||||
title: 'Daten importieren',
|
title: 'Daten importieren',
|
||||||
description: 'Laden Sie Charakterdaten aus VTT- oder CSV-Dateien hoch.',
|
description: 'Laden Sie Charakterdaten aus VTT- oder CSV-Dateien hoch.',
|
||||||
@@ -355,6 +371,7 @@ export default {
|
|||||||
cancel: 'Abbrechen'
|
cancel: 'Abbrechen'
|
||||||
},
|
},
|
||||||
search:'Suche',
|
search:'Suche',
|
||||||
|
newEntry:'Neuer Eintrag',
|
||||||
Skill:'Fertigkeit',
|
Skill:'Fertigkeit',
|
||||||
newSkill:'Neue Fertigkeit',
|
newSkill:'Neue Fertigkeit',
|
||||||
createSkill:'Fertigkeit erstellen',
|
createSkill:'Fertigkeit erstellen',
|
||||||
@@ -544,6 +561,9 @@ export default {
|
|||||||
lpRollTooltip: 'LP würfeln: 1d3 + 7 + (Ko/10)',
|
lpRollTooltip: 'LP würfeln: 1d3 + 7 + (Ko/10)',
|
||||||
apRollTooltip: 'AP würfeln: 3d6 + Modifikator je nach Klasse',
|
apRollTooltip: 'AP würfeln: 3d6 + Modifikator je nach Klasse',
|
||||||
bRollTooltip: 'B würfeln: 1d6 + Modifikator je nach Rasse'
|
bRollTooltip: 'B würfeln: 1d6 + Modifikator je nach Rasse'
|
||||||
|
},
|
||||||
|
datasheet: {
|
||||||
|
editHelp: 'Doppelklicken auf ein Feld, um es zu bearbeiten.'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
audit: {
|
audit: {
|
||||||
|
|||||||
+25
-5
@@ -1,10 +1,13 @@
|
|||||||
export default {
|
export default {
|
||||||
common: {
|
common: {
|
||||||
loading: 'Laden...',
|
loading: 'Loading...',
|
||||||
cancel: 'Abbrechen',
|
cancel: 'Cancel',
|
||||||
previous: 'Zurück',
|
previous: 'Previous',
|
||||||
next: 'Weiter',
|
next: 'Next',
|
||||||
back: 'Back'
|
back: 'Back',
|
||||||
|
save: 'Save',
|
||||||
|
saving: 'Saving...',
|
||||||
|
edit: 'Edit',
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
login: 'Login',
|
login: 'Login',
|
||||||
@@ -21,6 +24,19 @@ export default {
|
|||||||
dontHaveAccount: "Don't have an account?",
|
dontHaveAccount: "Don't have an account?",
|
||||||
registerHere: 'Register here'
|
registerHere: 'Register here'
|
||||||
},
|
},
|
||||||
|
forgotPassword: {
|
||||||
|
title: 'Reset password',
|
||||||
|
description: 'Enter your email address to receive a reset link.',
|
||||||
|
emailLabel: 'Email address',
|
||||||
|
emailPlaceholder: 'your{\'@\'}email.com',
|
||||||
|
submit: 'Send reset link',
|
||||||
|
submitting: 'Sending...',
|
||||||
|
successTitle: 'Email sent!',
|
||||||
|
successInfo: 'If an account exists for this email, a reset link has been sent.',
|
||||||
|
successHint: 'Check your inbox and follow the link.',
|
||||||
|
error: 'Error sending email. Please try again later.',
|
||||||
|
backToLogin: 'Back to login'
|
||||||
|
},
|
||||||
import: {
|
import: {
|
||||||
title: 'Import Data',
|
title: 'Import Data',
|
||||||
description: 'Upload character data from VTT or CSV files.',
|
description: 'Upload character data from VTT or CSV files.',
|
||||||
@@ -351,6 +367,7 @@ export default {
|
|||||||
cancel: 'Cancel'
|
cancel: 'Cancel'
|
||||||
},
|
},
|
||||||
search:'Suche',
|
search:'Suche',
|
||||||
|
newEntry:'New Entry',
|
||||||
Skill:'Fertigkeit',
|
Skill:'Fertigkeit',
|
||||||
newSkill:'New Skill',
|
newSkill:'New Skill',
|
||||||
createSkill:'Create Skill',
|
createSkill:'Create Skill',
|
||||||
@@ -540,6 +557,9 @@ export default {
|
|||||||
lpRollTooltip: 'Roll LP: 1d3 + 7 + (Ko/10)',
|
lpRollTooltip: 'Roll LP: 1d3 + 7 + (Ko/10)',
|
||||||
apRollTooltip: 'Roll AP: 3d6 + modifier based on class',
|
apRollTooltip: 'Roll AP: 3d6 + modifier based on class',
|
||||||
bRollTooltip: 'Roll B: 1d6 + modifier based on race'
|
bRollTooltip: 'Roll B: 1d6 + modifier based on race'
|
||||||
|
},
|
||||||
|
datasheet: {
|
||||||
|
editHelp: 'Click twice on a field to edit it.'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
audit: {
|
audit: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Frontend version information
|
// Frontend version information
|
||||||
export const VERSION = '0.2.2'
|
export const VERSION = '0.2.3'
|
||||||
|
|
||||||
// Git commit will be injected at build time or detected from env
|
// Git commit will be injected at build time or detected from env
|
||||||
export const GIT_COMMIT = import.meta.env.VITE_GIT_COMMIT || 'unknown'
|
export const GIT_COMMIT = import.meta.env.VITE_GIT_COMMIT || 'unknown'
|
||||||
|
|||||||
+206
-105
@@ -1,119 +1,220 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Script to update version number across the project
|
# Script to update, commit, and tag versions across the project
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if [ -z "$1" ]; then
|
BACKEND_VERSION_FILE="backend/appsystem/version.go"
|
||||||
echo "Usage: $0 <backend_version> [frontend_version] [auto]"
|
|
||||||
echo "Example: $0 0.1.31 # Sets both to 0.1.31"
|
|
||||||
echo "Example: $0 0.1.31 0.2.0 # Sets different versions"
|
|
||||||
echo "Example: $0 0.1.31 auto # Sets both to 0.1.31 and auto-commits"
|
|
||||||
echo "Example: $0 0.1.31 0.2.0 auto # Sets different versions and auto-commits"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
BACKEND_VERSION="$1"
|
|
||||||
AUTO_COMMIT=false
|
|
||||||
|
|
||||||
# Check if second argument is "auto"
|
|
||||||
if [ "$2" = "auto" ]; then
|
|
||||||
FRONTEND_VERSION="$1"
|
|
||||||
AUTO_COMMIT=true
|
|
||||||
elif [ -z "$2" ]; then
|
|
||||||
FRONTEND_VERSION="$1"
|
|
||||||
elif [ "$3" = "auto" ]; then
|
|
||||||
FRONTEND_VERSION="$2"
|
|
||||||
AUTO_COMMIT=true
|
|
||||||
else
|
|
||||||
FRONTEND_VERSION="$2"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if [ "$BACKEND_VERSION" = "$FRONTEND_VERSION" ]; then
|
|
||||||
echo "Updating both backend and frontend to version $BACKEND_VERSION..."
|
|
||||||
else
|
|
||||||
echo "Updating backend to $BACKEND_VERSION and frontend to $FRONTEND_VERSION..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update backend version
|
|
||||||
BACKEND_VERSION_FILE="backend/config/version.go"
|
|
||||||
if [ -f "$BACKEND_VERSION_FILE" ]; then
|
|
||||||
sed -i "s/const Version = \"[^\"]*\"/const Version = \"$BACKEND_VERSION\"/" "$BACKEND_VERSION_FILE"
|
|
||||||
echo "✓ Updated $BACKEND_VERSION_FILE to $BACKEND_VERSION"
|
|
||||||
else
|
|
||||||
echo "⚠ Warning: $BACKEND_VERSION_FILE not found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update frontend version
|
|
||||||
FRONTEND_VERSION_FILE="frontend/src/version.js"
|
FRONTEND_VERSION_FILE="frontend/src/version.js"
|
||||||
if [ -f "$FRONTEND_VERSION_FILE" ]; then
|
|
||||||
sed -i "s/export const VERSION = '[^']*'/export const VERSION = '$FRONTEND_VERSION'/" "$FRONTEND_VERSION_FILE"
|
|
||||||
echo "✓ Updated $FRONTEND_VERSION_FILE to $FRONTEND_VERSION"
|
|
||||||
else
|
|
||||||
echo "⚠ Warning: $FRONTEND_VERSION_FILE not found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update frontend package.json version
|
|
||||||
FRONTEND_PACKAGE="frontend/package.json"
|
FRONTEND_PACKAGE="frontend/package.json"
|
||||||
if [ -f "$FRONTEND_PACKAGE" ]; then
|
|
||||||
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$FRONTEND_VERSION\"/" "$FRONTEND_PACKAGE"
|
|
||||||
echo "✓ Updated $FRONTEND_PACKAGE to $FRONTEND_VERSION"
|
|
||||||
else
|
|
||||||
echo "⚠ Warning: $FRONTEND_PACKAGE not found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update VERSION.md files
|
|
||||||
BACKEND_VERSION_MD="backend/VERSION.md"
|
BACKEND_VERSION_MD="backend/VERSION.md"
|
||||||
if [ -f "$BACKEND_VERSION_MD" ]; then
|
|
||||||
sed -i "s/## Current Version: .*/## Current Version: $BACKEND_VERSION/" "$BACKEND_VERSION_MD"
|
|
||||||
echo "✓ Updated $BACKEND_VERSION_MD to $BACKEND_VERSION"
|
|
||||||
fi
|
|
||||||
|
|
||||||
FRONTEND_VERSION_MD="frontend/VERSION.md"
|
FRONTEND_VERSION_MD="frontend/VERSION.md"
|
||||||
if [ -f "$FRONTEND_VERSION_MD" ]; then
|
|
||||||
sed -i "s/## Current Version: .*/## Current Version: $FRONTEND_VERSION/" "$FRONTEND_VERSION_MD"
|
usage() {
|
||||||
echo "✓ Updated $FRONTEND_VERSION_MD to $FRONTEND_VERSION"
|
echo "Usage: $0 [-b backend_version] [-f frontend_version] [-n] [-c] [-t]"
|
||||||
|
echo " -b <version> Update backend version"
|
||||||
|
echo " -f <version> Update frontend version"
|
||||||
|
echo " -n Bump patch version based on current files"
|
||||||
|
echo " -c Commit using versions from files"
|
||||||
|
echo " -t Tag using versions from files"
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 -b 0.1.31 -f 0.2.0"
|
||||||
|
echo " $0 -b 0.1.31 -c -t"
|
||||||
|
echo " $0 -n -c"
|
||||||
|
echo " $0 -c -t"
|
||||||
|
echo "So you can set the version at any time, commit later without worrying about commit messages and tag later when merged into main."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
read_backend_version() {
|
||||||
|
if [ ! -f "$BACKEND_VERSION_FILE" ]; then
|
||||||
|
echo ""; return
|
||||||
|
fi
|
||||||
|
sed -n 's/.*const Version = "\(.*\)".*/\1/p' "$BACKEND_VERSION_FILE" | head -n1
|
||||||
|
}
|
||||||
|
|
||||||
|
read_frontend_version() {
|
||||||
|
if [ ! -f "$FRONTEND_VERSION_FILE" ]; then
|
||||||
|
echo ""; return
|
||||||
|
fi
|
||||||
|
sed -n "s/.*export const VERSION = '\(.*\)'.*/\1/p" "$FRONTEND_VERSION_FILE" | head -n1
|
||||||
|
}
|
||||||
|
|
||||||
|
bump_patch() {
|
||||||
|
local ver="$1"
|
||||||
|
|
||||||
|
if [[ "$ver" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||||
|
local major="${BASH_REMATCH[1]}"
|
||||||
|
local minor="${BASH_REMATCH[2]}"
|
||||||
|
local patch="${BASH_REMATCH[3]}"
|
||||||
|
patch=$((patch + 1))
|
||||||
|
echo "${major}.${minor}.${patch}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
BACKEND_VERSION_ARG=""
|
||||||
|
FRONTEND_VERSION_ARG=""
|
||||||
|
DO_COMMIT=false
|
||||||
|
DO_TAG=false
|
||||||
|
BUMP_PATCH=false
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-b)
|
||||||
|
[ -z "$2" ] && usage
|
||||||
|
BACKEND_VERSION_ARG="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-f)
|
||||||
|
[ -z "$2" ] && usage
|
||||||
|
FRONTEND_VERSION_ARG="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-c)
|
||||||
|
DO_COMMIT=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-t)
|
||||||
|
DO_TAG=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-n)
|
||||||
|
BUMP_PATCH=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$BACKEND_VERSION_ARG" ] && [ -z "$FRONTEND_VERSION_ARG" ] && [ "$DO_COMMIT" = false ] && [ "$DO_TAG" = false ] && [ "$BUMP_PATCH" = false ]; then
|
||||||
|
usage
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
if [ "$BUMP_PATCH" = true ]; then
|
||||||
echo "✅ Version update complete!"
|
BACKEND_VERSION_CURRENT=$(read_backend_version)
|
||||||
echo " Backend: $BACKEND_VERSION"
|
FRONTEND_VERSION_CURRENT=$(read_frontend_version)
|
||||||
echo " Frontend: $FRONTEND_VERSION"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Auto-commit if requested
|
if [ -z "$BACKEND_VERSION_CURRENT" ] && [ -z "$FRONTEND_VERSION_CURRENT" ]; then
|
||||||
if [ "$AUTO_COMMIT" = true ]; then
|
echo "❌ Cannot bump: version files missing" >&2
|
||||||
echo "Auto-committing changes..."
|
exit 1
|
||||||
if [ "$BACKEND_VERSION" = "$FRONTEND_VERSION" ]; then
|
|
||||||
git add backend/config/version.go frontend/src/version.js frontend/package.json frontend/VERSION.md
|
|
||||||
git commit -m "Bump version to $BACKEND_VERSION"
|
|
||||||
git tag v$BACKEND_VERSION
|
|
||||||
echo "✓ Committed and tagged as v$BACKEND_VERSION"
|
|
||||||
else
|
|
||||||
git add backend/config/version.go frontend/src/version.js frontend/package.json frontend/VERSION.md
|
|
||||||
git commit -m "Bump backend to $BACKEND_VERSION, frontend to $FRONTEND_VERSION"
|
|
||||||
git tag backend-v$BACKEND_VERSION
|
|
||||||
git tag frontend-v$FRONTEND_VERSION
|
|
||||||
git tag v$BACKEND_VERSION -m "Backend version $BACKEND_VERSION, Frontend version $FRONTEND_VERSION"
|
|
||||||
echo "✓ Committed and tagged as backend-v$BACKEND_VERSION, frontend-v$FRONTEND_VERSION, v$BACKEND_VERSION"
|
|
||||||
fi
|
fi
|
||||||
echo ""
|
|
||||||
echo "Next step:"
|
if [ -z "$BACKEND_VERSION_ARG" ] && [ -n "$BACKEND_VERSION_CURRENT" ]; then
|
||||||
echo " Push: git push && git push --tags"
|
NEXT_BACKEND_VERSION=$(bump_patch "$BACKEND_VERSION_CURRENT")
|
||||||
else
|
if [ -z "$NEXT_BACKEND_VERSION" ]; then
|
||||||
echo "Next steps:"
|
echo "❌ Cannot bump backend: invalid version format '$BACKEND_VERSION_CURRENT'" >&2
|
||||||
echo "1. Review changes: git diff"
|
exit 1
|
||||||
if [ "$BACKEND_VERSION" = "$FRONTEND_VERSION" ]; then
|
fi
|
||||||
echo "2. Commit changes: git commit -am 'Bump version to $BACKEND_VERSION'"
|
BACKEND_VERSION_ARG="$NEXT_BACKEND_VERSION"
|
||||||
echo "3. Tag release: git tag v$BACKEND_VERSION"
|
echo "✓ Bumping backend version to $BACKEND_VERSION_ARG"
|
||||||
git tag v$BACKEND_VERSION
|
fi
|
||||||
else
|
|
||||||
echo "2. Commit changes: git commit -am 'Bump backend to $BACKEND_VERSION, frontend to $FRONTEND_VERSION'"
|
if [ -z "$FRONTEND_VERSION_ARG" ] && [ -n "$FRONTEND_VERSION_CURRENT" ]; then
|
||||||
echo "3. Tag releases: git tag backend-v$BACKEND_VERSION && git tag frontend-v$FRONTEND_VERSION"
|
NEXT_FRONTEND_VERSION=$(bump_patch "$FRONTEND_VERSION_CURRENT")
|
||||||
git tag backend-v$BACKEND_VERSION
|
if [ -z "$NEXT_FRONTEND_VERSION" ]; then
|
||||||
git tag frontend-v$FRONTEND_VERSION
|
echo "❌ Cannot bump frontend: invalid version format '$FRONTEND_VERSION_CURRENT'" >&2
|
||||||
git tag v$BACKEND_VERSION -m "Backend version $BACKEND_VERSION, Frontend version $FRONTEND_VERSION"
|
exit 1
|
||||||
|
fi
|
||||||
|
FRONTEND_VERSION_ARG="$NEXT_FRONTEND_VERSION"
|
||||||
|
echo "✓ Bumping frontend version to $FRONTEND_VERSION_ARG"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$BACKEND_VERSION_ARG" ]; then
|
||||||
|
if [ -f "$BACKEND_VERSION_FILE" ]; then
|
||||||
|
sed -i "s/const Version = \"[^\"]*\"/const Version = \"$BACKEND_VERSION_ARG\"/" "$BACKEND_VERSION_FILE"
|
||||||
|
echo "✓ Updated $BACKEND_VERSION_FILE to $BACKEND_VERSION_ARG"
|
||||||
|
else
|
||||||
|
echo "⚠ Warning: $BACKEND_VERSION_FILE not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$BACKEND_VERSION_MD" ]; then
|
||||||
|
sed -i "s/## Current Version: .*/## Current Version: $BACKEND_VERSION_ARG/" "$BACKEND_VERSION_MD"
|
||||||
|
echo "✓ Updated $BACKEND_VERSION_MD to $BACKEND_VERSION_ARG"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$FRONTEND_VERSION_ARG" ]; then
|
||||||
|
if [ -f "$FRONTEND_VERSION_FILE" ]; then
|
||||||
|
sed -i "s/export const VERSION = '[^']*'/export const VERSION = '$FRONTEND_VERSION_ARG'/" "$FRONTEND_VERSION_FILE"
|
||||||
|
echo "✓ Updated $FRONTEND_VERSION_FILE to $FRONTEND_VERSION_ARG"
|
||||||
|
else
|
||||||
|
echo "⚠ Warning: $FRONTEND_VERSION_FILE not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$FRONTEND_PACKAGE" ]; then
|
||||||
|
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$FRONTEND_VERSION_ARG\"/" "$FRONTEND_PACKAGE"
|
||||||
|
echo "✓ Updated $FRONTEND_PACKAGE to $FRONTEND_VERSION_ARG"
|
||||||
|
else
|
||||||
|
echo "⚠ Warning: $FRONTEND_PACKAGE not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$FRONTEND_VERSION_MD" ]; then
|
||||||
|
sed -i "s/## Current Version: .*/## Current Version: $FRONTEND_VERSION_ARG/" "$FRONTEND_VERSION_MD"
|
||||||
|
echo "✓ Updated $FRONTEND_VERSION_MD to $FRONTEND_VERSION_ARG"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
BACKEND_VERSION_CURRENT=$(read_backend_version)
|
||||||
|
FRONTEND_VERSION_CURRENT=$(read_frontend_version)
|
||||||
|
|
||||||
|
if [ "$DO_COMMIT" = true ]; then
|
||||||
|
if [ -z "$BACKEND_VERSION_CURRENT" ] && [ -z "$FRONTEND_VERSION_CURRENT" ]; then
|
||||||
|
echo "❌ Cannot commit: version files missing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
FILES_TO_ADD=()
|
||||||
|
[ -f "$BACKEND_VERSION_FILE" ] && FILES_TO_ADD+=("$BACKEND_VERSION_FILE")
|
||||||
|
[ -f "$BACKEND_VERSION_MD" ] && FILES_TO_ADD+=("$BACKEND_VERSION_MD")
|
||||||
|
[ -f "$FRONTEND_VERSION_FILE" ] && FILES_TO_ADD+=("$FRONTEND_VERSION_FILE")
|
||||||
|
[ -f "$FRONTEND_PACKAGE" ] && FILES_TO_ADD+=("$FRONTEND_PACKAGE")
|
||||||
|
[ -f "$FRONTEND_VERSION_MD" ] && FILES_TO_ADD+=("$FRONTEND_VERSION_MD")
|
||||||
|
|
||||||
|
if [ ${#FILES_TO_ADD[@]} -eq 0 ]; then
|
||||||
|
echo "❌ Cannot commit: no files to add" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git add "${FILES_TO_ADD[@]}"
|
||||||
|
|
||||||
|
if [ -n "$BACKEND_VERSION_CURRENT" ] && [ -n "$FRONTEND_VERSION_CURRENT" ]; then
|
||||||
|
if [ "$BACKEND_VERSION_CURRENT" = "$FRONTEND_VERSION_CURRENT" ]; then
|
||||||
|
COMMIT_MSG="Bump version to $BACKEND_VERSION_CURRENT"
|
||||||
|
else
|
||||||
|
COMMIT_MSG="Bump backend to $BACKEND_VERSION_CURRENT, frontend to $FRONTEND_VERSION_CURRENT"
|
||||||
|
fi
|
||||||
|
elif [ -n "$BACKEND_VERSION_CURRENT" ]; then
|
||||||
|
COMMIT_MSG="Bump backend to $BACKEND_VERSION_CURRENT"
|
||||||
|
else
|
||||||
|
COMMIT_MSG="Bump frontend to $FRONTEND_VERSION_CURRENT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git commit -m "$COMMIT_MSG"
|
||||||
|
echo "✓ Committed: $COMMIT_MSG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DO_TAG" = true ]; then
|
||||||
|
if [ -z "$BACKEND_VERSION_CURRENT" ] && [ -z "$FRONTEND_VERSION_CURRENT" ]; then
|
||||||
|
echo "❌ Cannot tag: version files missing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$BACKEND_VERSION_CURRENT" ] && [ -n "$FRONTEND_VERSION_CURRENT" ] && [ "$BACKEND_VERSION_CURRENT" = "$FRONTEND_VERSION_CURRENT" ]; then
|
||||||
|
git tag "v$BACKEND_VERSION_CURRENT"
|
||||||
|
echo "✓ Tagged v$BACKEND_VERSION_CURRENT"
|
||||||
|
else
|
||||||
|
if [ -n "$BACKEND_VERSION_CURRENT" ]; then
|
||||||
|
git tag "backend-v$BACKEND_VERSION_CURRENT"
|
||||||
|
echo "✓ Tagged backend-v$BACKEND_VERSION_CURRENT"
|
||||||
|
fi
|
||||||
|
if [ -n "$FRONTEND_VERSION_CURRENT" ]; then
|
||||||
|
git tag "frontend-v$FRONTEND_VERSION_CURRENT"
|
||||||
|
echo "✓ Tagged frontend-v$FRONTEND_VERSION_CURRENT"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
echo "4. Push: git push && git push --tags"
|
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user