Compare commits
6 Commits
backend-v0.2.3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c999037485 | |||
| a7eb3cda81 | |||
| 1084a16eae | |||
| b3ed03bf40 | |||
| 3d0a1b86de | |||
| 6943e678a9 |
@@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
// Version is the application version
|
||||
const Version = "0.2.2"
|
||||
const Version = "0.2.4"
|
||||
|
||||
var (
|
||||
// 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{
|
||||
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)
|
||||
feC.Fertigkeiten = skills
|
||||
feC.InnateSkills = innateSkills
|
||||
@@ -199,6 +205,32 @@ func ToFeChar(object *models.Char) *models.FeChar {
|
||||
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) {
|
||||
var normSkills []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
|
||||
//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", GetLernCostNewSystem) // neuer Hauptendpunkt für alle Kostenberechnungen (verwendet neue Datenbank)
|
||||
charGrp.POST("/improve-skill-new", ImproveSkill) // Fertigkeit verbessern
|
||||
charGrp.POST("/improve-skill", ImproveSkill) // Fertigkeit verbessern
|
||||
|
||||
// Lernen und Verbessern (mit automatischem Audit-Log)
|
||||
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", LearnSpellOld) // Zauber lernen (altes System)
|
||||
charGrp.POST("/:id/learn-spell", LearnSpell) // Zauber lernen (altes System)
|
||||
|
||||
// Fertigkeiten-Information
|
||||
//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", 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-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", GetAvailableSpellsNewSystem) // Verfügbare Zauber mit Kosten (bereits gelernte ausgeschlossen)
|
||||
charGrp.GET("/spell-details", GetSpellDetails) // Detaillierte Informationen zu einem bestimmten Zauber
|
||||
|
||||
// Belohnungsarten für verschiedene Lernszenarien
|
||||
|
||||
@@ -32,6 +32,13 @@ type Config struct {
|
||||
// PDF Templates
|
||||
TemplatesDir string // Directory where PDF templates are stored
|
||||
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
|
||||
@@ -56,6 +63,11 @@ func defaultConfig() *Config {
|
||||
FrontendURL: "http://localhost:5173", // Default frontend URL for development
|
||||
TemplatesDir: "./templates", // Default templates 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
|
||||
}
|
||||
|
||||
// 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",
|
||||
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 (
|
||||
"bamort/logger"
|
||||
"bamort/mail"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
@@ -239,8 +240,7 @@ func generateResetHash() (string, error) {
|
||||
return hex.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
// sendResetEmail simuliert das Senden einer E-Mail (hier nur Logging)
|
||||
// In einer echten Implementierung würde hier ein E-Mail-Service verwendet
|
||||
// sendResetEmail sends a password reset email via SMTP
|
||||
func sendResetEmail(email, username, resetHash, frontendURL string) error {
|
||||
// Verwende die mitgegebene Frontend-URL oder fallback auf Standard
|
||||
baseURL := frontendURL
|
||||
@@ -250,25 +250,81 @@ func sendResetEmail(email, username, resetHash, frontendURL string) error {
|
||||
|
||||
resetLink := fmt.Sprintf("%s/reset-password?token=%s", baseURL, resetHash)
|
||||
|
||||
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 ===")
|
||||
// Create mail client
|
||||
mailClient := mail.NewClient()
|
||||
|
||||
// TODO: Hier echte E-Mail-Integration hinzufügen
|
||||
// z.B. SendGrid, SMTP, etc.
|
||||
// If mail is not configured, fallback to logging
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -19,3 +19,9 @@ API_PORT=8180
|
||||
BASE_URL=http://localhost:5173
|
||||
TEMPLATES_DIR=./templates
|
||||
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
|
||||
TEMPLATES_DIR=./templates
|
||||
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
|
||||
|
||||
## Current Version: 0.2.2
|
||||
## Current Version: 0.2.3
|
||||
|
||||
The frontend version is managed independently from the backend.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bamort-frontend",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.3",
|
||||
"private": true,
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"type": "module",
|
||||
|
||||
@@ -1674,7 +1674,7 @@ a:focus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
/* Modal content container */
|
||||
@@ -1727,11 +1727,13 @@ a:focus {
|
||||
/* Modal actions (alternative to footer) */
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #eee;
|
||||
padding: 20px 24px;
|
||||
border-top: 1px solid #dee2e6;
|
||||
background: #f8f9fa;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Close button */
|
||||
@@ -1778,6 +1780,8 @@ a:focus {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
@@ -4403,3 +4407,23 @@ a:focus {
|
||||
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;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
/*
|
||||
.help-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -561,7 +561,6 @@ export default {
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
/*cursor: help;*/
|
||||
background: #f5f5f5;
|
||||
color: #555;
|
||||
}
|
||||
@@ -570,4 +569,5 @@ export default {
|
||||
background: #e0e0e0;
|
||||
color: #222;
|
||||
}
|
||||
*/
|
||||
</style>
|
||||
|
||||
@@ -36,6 +36,14 @@
|
||||
<!-- Character Information -->
|
||||
<div class="character-info">
|
||||
<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>
|
||||
<strong>{{ $t('char') }}:</strong>
|
||||
<span
|
||||
|
||||
@@ -36,13 +36,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button @click="closeDialog" class="btn-cancel" :disabled="isExporting">
|
||||
{{ $t('export.cancel') }}
|
||||
</button>
|
||||
<button @click="performExport" class="btn-export" :disabled="!canExport || isExporting">
|
||||
<button @click="performExport" class="btn-primary btn-save" :disabled="!canExport || isExporting">
|
||||
<span v-if="!isExporting">{{ $t('export.export') }}</span>
|
||||
<span v-else>{{ $t('export.exporting') }}</span>
|
||||
</button>
|
||||
<button @click="closeDialog" class="btn-cancel" :disabled="isExporting">
|
||||
{{ $t('common.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,57 +1,56 @@
|
||||
<template>
|
||||
<div class="fullwidth-page" style="display: flex; justify-content: center; align-items: center; min-height: 100vh;">
|
||||
<div class="card" style="max-width: 400px; width: 100%; margin: 20px;">
|
||||
<div class="fullwidth-page reset-wrapper">
|
||||
<div class="card reset-card">
|
||||
<div class="page-header">
|
||||
<h2>Passwort zurücksetzen</h2>
|
||||
<p style="color: #666; font-size: 0.9em; margin-top: 10px;">
|
||||
Geben Sie Ihre E-Mail-Adresse ein, um einen Reset-Link zu erhalten.
|
||||
<h2>{{ $t('forgotPassword.title') }}</h2>
|
||||
<p class="reset-description">
|
||||
{{ $t('forgotPassword.description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="requestReset" v-if="!submitted">
|
||||
<div class="form-group">
|
||||
<label for="email">E-Mail-Adresse</label>
|
||||
<label for="email">{{ $t('forgotPassword.emailLabel') }}</label>
|
||||
<input
|
||||
v-model="email"
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
class="form-control"
|
||||
placeholder="ihre@email.de"
|
||||
:placeholder="$t('forgotPassword.emailPlaceholder')"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
style="width: 100%; margin-top: 10px;"
|
||||
class="btn btn-primary reset-button"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<span v-if="isLoading">Wird gesendet...</span>
|
||||
<span v-else>Reset-Link senden</span>
|
||||
<span v-if="isLoading">{{ $t('forgotPassword.submitting') }}</span>
|
||||
<span v-else>{{ $t('forgotPassword.submit') }}</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div v-if="submitted" class="badge badge-success" style="width: 100%; margin-top: 15px; text-align: center; display: block;">
|
||||
<p style="margin: 10px 0;">
|
||||
<strong>E-Mail gesendet!</strong>
|
||||
<div v-if="submitted" class="badge badge-success reset-badge">
|
||||
<p class="reset-success-title">
|
||||
<strong>{{ $t('forgotPassword.successTitle') }}</strong>
|
||||
</p>
|
||||
<p style="font-size: 0.9em; margin: 5px 0;">
|
||||
Falls ein Account mit dieser E-Mail-Adresse existiert, wurde ein Reset-Link gesendet.
|
||||
<p class="reset-success-text">
|
||||
{{ $t('forgotPassword.successInfo') }}
|
||||
</p>
|
||||
<p style="font-size: 0.8em; margin: 5px 0; opacity: 0.8;">
|
||||
Prüfen Sie Ihre E-Mails und folgen Sie dem Link.
|
||||
<p class="reset-success-hint">
|
||||
{{ $t('forgotPassword.successHint') }}
|
||||
</p>
|
||||
</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 }}
|
||||
</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">
|
||||
Zurück zum Login
|
||||
{{ $t('forgotPassword.backToLogin') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,7 +85,7 @@ export default {
|
||||
console.log('Password reset email requested for:', this.email)
|
||||
} catch (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 {
|
||||
this.isLoading = false
|
||||
}
|
||||
@@ -95,6 +94,57 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* All common styles moved to main.css */
|
||||
<style scoped>
|
||||
.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>
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
<div class="tables-container">
|
||||
<div class="table-wrapper-left">
|
||||
<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 -->
|
||||
<div v-if="isOwner" class="learning-mode-controls">
|
||||
<!-- Ressourcen-Anzeige (nur sichtbar wenn Lernmodus aktiv) -->
|
||||
@@ -64,8 +73,42 @@
|
||||
</tr>
|
||||
<template v-for="skill in skills">
|
||||
<tr>
|
||||
<td>{{ skill.name || '-' }}</td>
|
||||
<td>{{ skill.fertigkeitswert || '-' }}</td>
|
||||
<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 class="pp-cell">
|
||||
<div v-if="isOwner" class="pp-container">
|
||||
@@ -88,7 +131,23 @@
|
||||
</div>
|
||||
<span v-else>{{ skill.pp || '0' }}</span>
|
||||
</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">
|
||||
<button
|
||||
@click="improveSkill(skill)"
|
||||
@@ -106,8 +165,42 @@
|
||||
</tr>
|
||||
<template v-for="skill in character.waffenfertigkeiten">
|
||||
<tr>
|
||||
<td>{{ skill.name || '-' }}</td>
|
||||
<td>{{ skill.fertigkeitswert || '-' }}</td>
|
||||
<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 class="pp-cell">
|
||||
<div v-if="isOwner" class="pp-container">
|
||||
@@ -130,7 +223,23 @@
|
||||
</div>
|
||||
<span v-else>{{ skill.pp || '0' }}</span>
|
||||
</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">
|
||||
<button
|
||||
@click="improveWeaponSkill(skill)"
|
||||
@@ -318,6 +427,11 @@ export default {
|
||||
selectedSkillToLearn: null,
|
||||
selectedLearningType: 'improve', // 'improve', 'learn', 'spell'
|
||||
|
||||
// Inline editing
|
||||
editingSkillId: null,
|
||||
editingField: null,
|
||||
editValue: '',
|
||||
|
||||
isLoading: false
|
||||
};
|
||||
},
|
||||
@@ -623,6 +737,72 @@ export default {
|
||||
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 class="modal-footer">
|
||||
<button @click="closeDialog" class="btn-cancel" :disabled="isUpdating">
|
||||
{{ $t('visibility.cancel') }}
|
||||
<button @click="updateVisibilityAndShares" class="btn-primary btn-save" :disabled="isUpdating">
|
||||
<span v-if="!isUpdating">{{ $t('common.save') }}</span>
|
||||
<span v-else>{{ $t('common.saving') }}</span>
|
||||
</button>
|
||||
<button @click="updateVisibilityAndShares" class="btn-primary" :disabled="isUpdating">
|
||||
<span v-if="!isUpdating">{{ $t('visibility.save') }}</span>
|
||||
<span v-else>{{ $t('visibility.saving') }}</span>
|
||||
<button @click="closeDialog" class="btn-cancel" :disabled="isUpdating">
|
||||
{{ $t('common.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
type="text"
|
||||
:placeholder="$t('search')"
|
||||
/>
|
||||
<button class="btn-primary" @click="startCreate">{{ $t('newEntry') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,6 +32,65 @@
|
||||
<td colspan="7">{{ $t('common.loading') }}</td>
|
||||
</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">
|
||||
<tr v-if="editingId !== believe.id">
|
||||
<td>{{ believe.id }}</td>
|
||||
@@ -40,7 +100,7 @@
|
||||
<td>{{ believe.page_number || '-' }}</td>
|
||||
<td>{{ getSystemCodeById(believe.game_system_id, believe.game_system) || '-' }}</td>
|
||||
<td>
|
||||
<button @click="startEdit(believe)">{{ $t('believe.edit') }}</button>
|
||||
<button @click="startEdit(believe)">{{ $t('common.edit') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
@@ -82,19 +142,19 @@
|
||||
|
||||
<div class="edit-actions">
|
||||
<button
|
||||
class="btn-primary"
|
||||
class="btn-primary btn-save"
|
||||
:disabled="isSaving"
|
||||
@click="saveEdit"
|
||||
>
|
||||
<span v-if="!isSaving">{{ $t('believe.save') }}</span>
|
||||
<span v-else>{{ $t('believe.saving') }}</span>
|
||||
<span v-if="!isSaving">{{ $t('common.save') }}</span>
|
||||
<span v-else>{{ $t('common.saving') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn-cancel"
|
||||
:disabled="isSaving"
|
||||
@click="cancelEdit"
|
||||
>
|
||||
{{ $t('believe.cancel') }}
|
||||
{{ $t('common.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,6 +239,9 @@ export default {
|
||||
editedItem: null,
|
||||
gameSystems: [],
|
||||
selectedSystemId: null,
|
||||
creatingNew: false,
|
||||
newItem: null,
|
||||
createSelectedSystemId: null,
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
error: '',
|
||||
@@ -251,6 +314,23 @@ export default {
|
||||
findSystemIdByCode(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() {
|
||||
if (!this.editedItem || !this.editingId) {
|
||||
return
|
||||
@@ -294,6 +374,45 @@ export default {
|
||||
} finally {
|
||||
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"
|
||||
:placeholder="`${$t('search')} ${$t('Equipment')}...`"
|
||||
/>
|
||||
<button @click="startCreate" class="btn-primary">{{ $t('newEntry') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,6 +53,68 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<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">
|
||||
<tr v-if="editingIndex !== index">
|
||||
<td>{{ dtaItem.id || '' }}</td>
|
||||
@@ -64,7 +127,7 @@
|
||||
<td><input type="checkbox" :checked="dtaItem.personal_item" disabled /></td>
|
||||
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.system || 'midgard') }}</td>
|
||||
<td>
|
||||
<button @click="startEdit(index)">Edit</button>
|
||||
<button @click="startEdit(index)">{{ $t('common.edit') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Edit Mode -->
|
||||
@@ -125,8 +188,8 @@
|
||||
</div>
|
||||
|
||||
<div class="edit-actions">
|
||||
<button @click="saveEdit(index)" class="btn-save">Save</button>
|
||||
<button @click="cancelEdit" class="btn-cancel">Cancel</button>
|
||||
<button @click="saveEdit(index)" class="btn-save">{{ $t('common.save') }}</button>
|
||||
<button @click="cancelEdit" class="btn-cancel">{{ $t('common.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -216,7 +279,10 @@ export default {
|
||||
enhancedEquipment: [],
|
||||
availableSources: [],
|
||||
gameSystems: [],
|
||||
selectedSystemId: null
|
||||
selectedSystemId: null,
|
||||
creatingNew: false,
|
||||
newItem: null,
|
||||
createSelectedSystemId: null
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
@@ -352,6 +418,57 @@ export default {
|
||||
this.editedItem = 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) {
|
||||
return findSystemIdByCode(this.gameSystems, code)
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<h2>{{ $t('maintenance') }} - {{ $t('gamesystem.title') }}</h2>
|
||||
<div class="search-box">
|
||||
<input v-model="searchTerm" type="text" :placeholder="$t('search')" />
|
||||
<button class="btn-primary" @click="startCreate">{{ $t('newEntry') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +27,39 @@
|
||||
<td colspan="6">{{ $t('common.loading') }}</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="creatingNew">
|
||||
<td>New</td>
|
||||
<td colspan="5">
|
||||
<div class="edit-form">
|
||||
<div class="edit-row">
|
||||
<label>{{ $t('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">
|
||||
<tr v-if="editingId !== gs.id">
|
||||
<td>{{ gs.id }}</td>
|
||||
@@ -33,7 +67,7 @@
|
||||
<td>{{ gs.name }}</td>
|
||||
<td>{{ gs.description || '-' }}</td>
|
||||
<td><input type="checkbox" :checked="gs.is_active" disabled /></td>
|
||||
<td><button @click="startEdit(gs)">{{ $t('gamesystem.edit') }}</button></td>
|
||||
<td><button @click="startEdit(gs)">{{ $t('common.edit') }}</button></td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td>{{ gs.id }}</td>
|
||||
@@ -53,12 +87,12 @@
|
||||
<input type="checkbox" v-model="editedItem.is_active" />
|
||||
</div>
|
||||
<div class="edit-actions">
|
||||
<button class="btn-primary" :disabled="isSaving" @click="saveEdit">
|
||||
<span v-if="!isSaving">{{ $t('gamesystem.save') }}</span>
|
||||
<span v-else>{{ $t('gamesystem.saving') }}</span>
|
||||
<button class="btn-primary btn-save" :disabled="isSaving" @click="saveEdit">
|
||||
<span v-if="!isSaving">{{ $t('common.save') }}</span>
|
||||
<span v-else>{{ $t('common.saving') }}</span>
|
||||
</button>
|
||||
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
|
||||
{{ $t('gamesystem.cancel') }}
|
||||
{{ $t('common.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,6 +141,8 @@ export default {
|
||||
systems: [],
|
||||
editingId: null,
|
||||
editedItem: null,
|
||||
creatingNew: false,
|
||||
newItem: null,
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
error: '',
|
||||
@@ -149,6 +185,20 @@ export default {
|
||||
this.editingId = 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() {
|
||||
if (!this.editedItem) return
|
||||
const payload = {
|
||||
@@ -169,6 +219,32 @@ export default {
|
||||
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>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-primary" @click="startCreate">{{ $t('newEntry') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-box">{{ error }}</div>
|
||||
@@ -38,6 +39,60 @@
|
||||
<td colspan="10">{{ $t('common.loading') }}</td>
|
||||
</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">
|
||||
<tr v-if="editingId !== src.id">
|
||||
<td>{{ src.id }}</td>
|
||||
@@ -49,7 +104,7 @@
|
||||
<td>{{ src.publish_year }}</td>
|
||||
<td><input type="checkbox" :checked="src.is_active" disabled /></td>
|
||||
<td><input type="checkbox" :checked="src.is_core" disabled /></td>
|
||||
<td><button @click="startEdit(src)">{{ $t('litsource.edit') }}</button></td>
|
||||
<td><button @click="startEdit(src)">{{ $t('common.edit') }}</button></td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td>{{ src.id }}</td>
|
||||
@@ -92,12 +147,12 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="edit-actions">
|
||||
<button class="btn-primary" :disabled="isSaving" @click="saveEdit">
|
||||
<span v-if="!isSaving">{{ $t('litsource.save') }}</span>
|
||||
<span v-else>{{ $t('litsource.saving') }}</span>
|
||||
<button class="btn-primary btn-save" :disabled="isSaving" @click="saveEdit">
|
||||
<span v-if="!isSaving">{{ $t('common.save') }}</span>
|
||||
<span v-else>{{ $t('common.saving') }}</span>
|
||||
</button>
|
||||
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
|
||||
{{ $t('litsource.cancel') }}
|
||||
{{ $t('common.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,6 +214,9 @@ export default {
|
||||
sources: [],
|
||||
editingId: null,
|
||||
editedItem: null,
|
||||
creatingNew: false,
|
||||
newItem: null,
|
||||
createSelectedSystemId: null,
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
error: '',
|
||||
@@ -237,6 +295,28 @@ export default {
|
||||
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) {
|
||||
return findSystemById(this.gameSystems, id)
|
||||
},
|
||||
@@ -272,6 +352,35 @@ export default {
|
||||
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>
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
<h2>{{ $t('maintenance') }} - {{ $t('misc.title') }}</h2>
|
||||
<div class="search-box">
|
||||
<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>
|
||||
|
||||
@@ -27,6 +32,49 @@
|
||||
<td colspan="7">{{ $t('common.loading') }}</td>
|
||||
</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">
|
||||
<tr v-if="editingId !== item.id">
|
||||
<td>{{ item.id }}</td>
|
||||
@@ -35,7 +83,7 @@
|
||||
<td>{{ sourceCodeFor(item.source_id) }}</td>
|
||||
<td>{{ item.page_number || '-' }}</td>
|
||||
<td>{{ systemCodeFor(item.game_system_id, item.game_system) || '-' }}</td>
|
||||
<td><button @click="startEdit(item)">{{ $t('misc.edit') }}</button></td>
|
||||
<td><button @click="startEdit(item)">{{ $t('common.edit') }}</button></td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td>{{ item.id }}</td>
|
||||
@@ -68,12 +116,12 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="edit-actions">
|
||||
<button class="btn-primary" :disabled="isSaving" @click="saveEdit">
|
||||
<span v-if="!isSaving">{{ $t('misc.save') }}</span>
|
||||
<span v-else>{{ $t('misc.saving') }}</span>
|
||||
<button class="btn-primary btn-save" :disabled="isSaving" @click="saveEdit">
|
||||
<span v-if="!isSaving">{{ $t('common.save') }}</span>
|
||||
<span v-else>{{ $t('common.saving') }}</span>
|
||||
</button>
|
||||
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
|
||||
{{ $t('misc.cancel') }}
|
||||
{{ $t('common.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,10 +184,14 @@ export default {
|
||||
editingId: null,
|
||||
editedItem: null,
|
||||
selectedSystemId: null,
|
||||
creatingNew: false,
|
||||
newItem: null,
|
||||
createSelectedSystemId: null,
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
error: '',
|
||||
searchTerm: '',
|
||||
filterKey: '',
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
@@ -148,12 +200,17 @@ export default {
|
||||
computed: {
|
||||
filteredItems() {
|
||||
const term = this.searchTerm.trim().toLowerCase()
|
||||
const list = term
|
||||
let list = term
|
||||
? this.items.filter(it =>
|
||||
(it.key || '').toLowerCase().includes(term) ||
|
||||
(it.value || '').toLowerCase().includes(term)
|
||||
)
|
||||
: this.items
|
||||
|
||||
if (this.filterKey) {
|
||||
list = list.filter(it => it.key === this.filterKey)
|
||||
}
|
||||
|
||||
return [...list].sort((a, b) => (a.key || '').localeCompare(b.key || ''))
|
||||
},
|
||||
keyOptions() {
|
||||
@@ -199,6 +256,23 @@ export default {
|
||||
await this.loadSources()
|
||||
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() {
|
||||
try {
|
||||
const systems = await fetchGameSystems()
|
||||
@@ -279,6 +353,27 @@ export default {
|
||||
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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="header-section">
|
||||
<h2>{{ $t('maintenance') }} - {{ $t('skillimprovement.title') }}</h2>
|
||||
<button class="btn-primary" @click="startCreate">{{ $t('newEntry') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-box">{{ error }}</div>
|
||||
@@ -22,6 +23,42 @@
|
||||
<tr v-if="isLoading">
|
||||
<td colspan="6">{{ $t('common.loading') }}</td>
|
||||
</tr>
|
||||
<tr v-if="creatingNew">
|
||||
<td>New</td>
|
||||
<td colspan="5">
|
||||
<div class="edit-form">
|
||||
<div class="edit-row">
|
||||
<label>{{ $t('skillimprovement.level') }}</label>
|
||||
<input v-model.number="newItem.current_level" type="number" />
|
||||
<label class="inline-label">{{ $t('skillimprovement.te') }}</label>
|
||||
<input v-model.number="newItem.te_required" type="number" />
|
||||
</div>
|
||||
<div class="edit-row">
|
||||
<label>{{ $t('skillimprovement.category') }}</label>
|
||||
<select v-model.number="newItem.category_id">
|
||||
<option v-for="cat in categoryOptions" :key="cat.id" :value="cat.id">
|
||||
{{ cat.label }}
|
||||
</option>
|
||||
</select>
|
||||
<label class="inline-label">{{ $t('skillimprovement.difficulty') }}</label>
|
||||
<select v-model.number="newItem.difficulty_id">
|
||||
<option v-for="diff in difficultyOptions" :key="diff.id" :value="diff.id">
|
||||
{{ diff.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="edit-actions">
|
||||
<button class="btn-primary btn-save" :disabled="isSaving" @click="saveCreate">
|
||||
<span v-if="!isSaving">{{ $t('common.save') }}</span>
|
||||
<span v-else>{{ $t('common.saving') }}</span>
|
||||
</button>
|
||||
<button class="btn-cancel" :disabled="isSaving" @click="cancelCreate">
|
||||
{{ $t('common.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-for="cost in costs" :key="cost.id">
|
||||
<tr v-if="editingId !== cost.id">
|
||||
<td>{{ cost.id }}</td>
|
||||
@@ -29,7 +66,7 @@
|
||||
<td>{{ cost.te_required }}</td>
|
||||
<td>{{ displayCategory(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 v-else>
|
||||
<td>{{ cost.id }}</td>
|
||||
@@ -56,12 +93,12 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="edit-actions">
|
||||
<button class="btn-primary" :disabled="isSaving" @click="saveEdit">
|
||||
<span v-if="!isSaving">{{ $t('skillimprovement.save') }}</span>
|
||||
<span v-else>{{ $t('skillimprovement.saving') }}</span>
|
||||
<button class="btn-primary btn-save" :disabled="isSaving" @click="saveEdit">
|
||||
<span v-if="!isSaving">{{ $t('common.save') }}</span>
|
||||
<span v-else>{{ $t('common.saving') }}</span>
|
||||
</button>
|
||||
<button class="btn-cancel" :disabled="isSaving" @click="cancelEdit">
|
||||
{{ $t('skillimprovement.cancel') }}
|
||||
{{ $t('common.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,6 +150,8 @@ export default {
|
||||
costs: [],
|
||||
editingId: null,
|
||||
editedItem: null,
|
||||
creatingNew: false,
|
||||
newItem: null,
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
error: '',
|
||||
@@ -177,6 +216,22 @@ export default {
|
||||
this.editingId = 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() {
|
||||
if (!this.editedItem) return
|
||||
const payload = {
|
||||
@@ -198,6 +253,26 @@ export default {
|
||||
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>
|
||||
|
||||
@@ -209,8 +209,8 @@
|
||||
</div>
|
||||
|
||||
<div class="edit-actions">
|
||||
<button @click="saveCreate" class="btn-save">{{ $t('createSkill') }}</button>
|
||||
<button @click="cancelCreate" class="btn-cancel">Cancel</button>
|
||||
<button @click="saveCreate" class="btn-save">{{ $t('common.save') }}</button>
|
||||
<button @click="cancelCreate" class="btn-cancel">{{ $t('common.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -232,7 +232,7 @@
|
||||
<td>{{ formatQuelle(dtaItem) }}</td>
|
||||
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.game_system || 'midgard') }}</td>
|
||||
<td>
|
||||
<button @click="startEdit(index)">Edit</button>
|
||||
<button @click="startEdit(index)">{{ $t('common.edit') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -340,8 +340,8 @@
|
||||
</div>
|
||||
|
||||
<div class="edit-actions">
|
||||
<button @click="saveEdit(index)" class="btn-save">Save</button>
|
||||
<button @click="cancelEdit" class="btn-cancel">Cancel</button>
|
||||
<button @click="saveEdit(index)" class="btn-save">{{ $t('common.save') }}</button>
|
||||
<button @click="cancelEdit" class="btn-cancel">{{ $t('common.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
v-model="searchTerm"
|
||||
:placeholder="`${$t('search')} ${$t('Spell')}...`"
|
||||
/>
|
||||
<button @click="startCreate" class="btn-primary">{{ $t('newEntry') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,6 +40,32 @@
|
||||
{{ $t('spell.import') || 'Import Spells' }}
|
||||
</button>
|
||||
<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 }}
|
||||
<span v-if="importResult.total_spells"> ({{ importResult.total_spells }} spells total)</span>
|
||||
</div>
|
||||
@@ -121,6 +148,102 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<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">
|
||||
<tr v-if="editingIndex !== index">
|
||||
<td>{{ dtaItem.id || '' }}</td>
|
||||
@@ -138,7 +261,7 @@
|
||||
<td>{{ formatQuelle(dtaItem) }}</td>
|
||||
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.system || 'midgard') }}</td>
|
||||
<td>
|
||||
<button @click="startEdit(index)">Edit</button>
|
||||
<button @click="startEdit(index)">{{ $t('common.edit') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Edit Mode -->
|
||||
@@ -233,8 +356,8 @@
|
||||
</div>
|
||||
|
||||
<div class="edit-actions">
|
||||
<button @click="saveEdit(index)" class="btn-save">Save</button>
|
||||
<button @click="cancelEdit" class="btn-cancel">Cancel</button>
|
||||
<button @click="saveEdit(index)" class="btn-save">{{ $t('common.save') }}</button>
|
||||
<button @click="cancelEdit" class="btn-cancel">{{ $t('common.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -397,7 +520,10 @@ export default {
|
||||
enhancedSpells: [],
|
||||
availableSources: [],
|
||||
gameSystems: [],
|
||||
selectedSystemId: null
|
||||
selectedSystemId: null,
|
||||
creatingNew: false,
|
||||
newItem: null,
|
||||
createSelectedSystemId: null
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
@@ -592,6 +718,33 @@ export default {
|
||||
this.editedItem = 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) {
|
||||
return findSystemIdByCode(this.gameSystems, code)
|
||||
},
|
||||
@@ -605,25 +758,51 @@ export default {
|
||||
},
|
||||
async handleSpellUpdate({ index, spell }) {
|
||||
try {
|
||||
const response = await API.put(
|
||||
`/api/maintenance/spells/${spell.id}`, spell,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}` ,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
const response = await API.put(
|
||||
`/api/maintenance/spells/${spell.id}`, spell,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}` ,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
)
|
||||
if (!response.statusText== "OK") throw new Error('Update failed');
|
||||
const updatedSkill = response.data;
|
||||
// Update the spell in mdata
|
||||
this.mdata.spells = this.mdata.spells.map(s =>
|
||||
s.id === updatedSkill.id ? updatedSkill : s
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to update spell:', error);
|
||||
}
|
||||
)
|
||||
if (!response.statusText== "OK") throw new Error('Update failed');
|
||||
const updatedSkill = response.data;
|
||||
// Update the spell in mdata
|
||||
this.mdata.spells = this.mdata.spells.map(s =>
|
||||
s.id === updatedSkill.id ? updatedSkill : s
|
||||
);
|
||||
} 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) {
|
||||
const file = event.target.files[0];
|
||||
if (file && file.type === 'text/csv') {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
v-model="searchTerm"
|
||||
:placeholder="`${$t('search')} ${$t('WaeponSkill')}...`"
|
||||
/>
|
||||
<button @click="startCreate" class="btn-primary">{{ $t('newEntry') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,6 +48,69 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<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">
|
||||
<tr v-if="editingIndex !== index">
|
||||
<td>{{ dtaItem.id || '' }}</td>
|
||||
@@ -57,7 +121,7 @@
|
||||
<td>{{ formatQuelle(dtaItem) }}</td>
|
||||
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.system || 'midgard') }}</td>
|
||||
<td>
|
||||
<button @click="startEdit(index)">Edit</button>
|
||||
<button @click="startEdit(index)">{{ $t('common.edit') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Edit Mode -->
|
||||
@@ -119,8 +183,8 @@
|
||||
</div>
|
||||
|
||||
<div class="edit-actions">
|
||||
<button @click="saveEdit(index)" class="btn-save">Save</button>
|
||||
<button @click="cancelEdit" class="btn-cancel">Cancel</button>
|
||||
<button @click="saveEdit(index)" class="btn-save">{{ $t('common.save') }}</button>
|
||||
<button @click="cancelEdit" class="btn-cancel">{{ $t('common.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -211,7 +275,10 @@ export default {
|
||||
availableSources: [],
|
||||
availableDifficultiesData: [],
|
||||
gameSystems: [],
|
||||
selectedSystemId: null
|
||||
selectedSystemId: null,
|
||||
creatingNew: false,
|
||||
newItem: null,
|
||||
createSelectedSystemId: null
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
@@ -345,6 +412,53 @@ export default {
|
||||
this.editedItem = 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) {
|
||||
return findSystemIdByCode(this.gameSystems, code)
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
v-model="searchTerm"
|
||||
:placeholder="`${$t('search')} ${$t('Weapons')}...`"
|
||||
/>
|
||||
<button @click="startCreate" class="btn-primary">{{ $t('newEntry') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,6 +83,110 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<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">
|
||||
<tr v-if="editingIndex !== index">
|
||||
<td>{{ dtaItem.id || '' }}</td>
|
||||
@@ -98,7 +203,7 @@
|
||||
<td><input type="checkbox" :checked="dtaItem.personal_item" disabled /></td>
|
||||
<td>{{ getSystemCodeById(dtaItem.game_system_id, dtaItem.system || 'midgard') }}</td>
|
||||
<td>
|
||||
<button @click="startEdit(index)">Edit</button>
|
||||
<button @click="startEdit(index)">{{ $t('common.edit') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Edit Mode -->
|
||||
@@ -201,8 +306,8 @@
|
||||
</div>
|
||||
|
||||
<div class="edit-actions">
|
||||
<button @click="saveEdit(index)" class="btn-save">Save</button>
|
||||
<button @click="cancelEdit" class="btn-cancel">Cancel</button>
|
||||
<button @click="saveEdit(index)" class="btn-save">{{ $t('common.save') }}</button>
|
||||
<button @click="cancelEdit" class="btn-cancel">{{ $t('common.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -296,7 +401,10 @@ export default {
|
||||
enhancedWeapons: [],
|
||||
availableSources: [],
|
||||
gameSystems: [],
|
||||
selectedSystemId: null
|
||||
selectedSystemId: null,
|
||||
creatingNew: false,
|
||||
newItem: null,
|
||||
createSelectedSystemId: null
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
@@ -478,6 +586,59 @@ export default {
|
||||
this.editedItem = 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) {
|
||||
return findSystemIdByCode(this.gameSystems, code)
|
||||
},
|
||||
|
||||
@@ -5,6 +5,9 @@ export default {
|
||||
previous: 'Zurück',
|
||||
next: 'Weiter',
|
||||
back: 'Zurück',
|
||||
save: 'Speichern',
|
||||
saving: 'Speichern...',
|
||||
edit: 'Bearbeiten',
|
||||
},
|
||||
auth: {
|
||||
login: 'Anmelden',
|
||||
@@ -21,6 +24,19 @@ export default {
|
||||
dontHaveAccount: 'Noch kein Konto?',
|
||||
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: {
|
||||
title: 'Daten importieren',
|
||||
description: 'Laden Sie Charakterdaten aus VTT- oder CSV-Dateien hoch.',
|
||||
@@ -355,6 +371,7 @@ export default {
|
||||
cancel: 'Abbrechen'
|
||||
},
|
||||
search:'Suche',
|
||||
newEntry:'Neuer Eintrag',
|
||||
Skill:'Fertigkeit',
|
||||
newSkill:'Neue Fertigkeit',
|
||||
createSkill:'Fertigkeit erstellen',
|
||||
@@ -544,6 +561,9 @@ export default {
|
||||
lpRollTooltip: 'LP würfeln: 1d3 + 7 + (Ko/10)',
|
||||
apRollTooltip: 'AP würfeln: 3d6 + Modifikator je nach Klasse',
|
||||
bRollTooltip: 'B würfeln: 1d6 + Modifikator je nach Rasse'
|
||||
},
|
||||
datasheet: {
|
||||
editHelp: 'Doppelklicken auf ein Feld, um es zu bearbeiten.'
|
||||
}
|
||||
},
|
||||
audit: {
|
||||
|
||||
+25
-5
@@ -1,10 +1,13 @@
|
||||
export default {
|
||||
common: {
|
||||
loading: 'Laden...',
|
||||
cancel: 'Abbrechen',
|
||||
previous: 'Zurück',
|
||||
next: 'Weiter',
|
||||
back: 'Back'
|
||||
loading: 'Loading...',
|
||||
cancel: 'Cancel',
|
||||
previous: 'Previous',
|
||||
next: 'Next',
|
||||
back: 'Back',
|
||||
save: 'Save',
|
||||
saving: 'Saving...',
|
||||
edit: 'Edit',
|
||||
},
|
||||
auth: {
|
||||
login: 'Login',
|
||||
@@ -21,6 +24,19 @@ export default {
|
||||
dontHaveAccount: "Don't have an account?",
|
||||
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: {
|
||||
title: 'Import Data',
|
||||
description: 'Upload character data from VTT or CSV files.',
|
||||
@@ -351,6 +367,7 @@ export default {
|
||||
cancel: 'Cancel'
|
||||
},
|
||||
search:'Suche',
|
||||
newEntry:'New Entry',
|
||||
Skill:'Fertigkeit',
|
||||
newSkill:'New Skill',
|
||||
createSkill:'Create Skill',
|
||||
@@ -540,6 +557,9 @@ export default {
|
||||
lpRollTooltip: 'Roll LP: 1d3 + 7 + (Ko/10)',
|
||||
apRollTooltip: 'Roll AP: 3d6 + modifier based on class',
|
||||
bRollTooltip: 'Roll B: 1d6 + modifier based on race'
|
||||
},
|
||||
datasheet: {
|
||||
editHelp: 'Click twice on a field to edit it.'
|
||||
}
|
||||
},
|
||||
audit: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// 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
|
||||
export const GIT_COMMIT = import.meta.env.VITE_GIT_COMMIT || 'unknown'
|
||||
|
||||
+206
-105
@@ -1,119 +1,220 @@
|
||||
#!/bin/bash
|
||||
# Script to update version number across the project
|
||||
# Script to update, commit, and tag versions across the project
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
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
|
||||
BACKEND_VERSION_FILE="backend/appsystem/version.go"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
if [ -f "$FRONTEND_VERSION_MD" ]; then
|
||||
sed -i "s/## Current Version: .*/## Current Version: $FRONTEND_VERSION/" "$FRONTEND_VERSION_MD"
|
||||
echo "✓ Updated $FRONTEND_VERSION_MD to $FRONTEND_VERSION"
|
||||
|
||||
usage() {
|
||||
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
|
||||
|
||||
echo ""
|
||||
echo "✅ Version update complete!"
|
||||
echo " Backend: $BACKEND_VERSION"
|
||||
echo " Frontend: $FRONTEND_VERSION"
|
||||
echo ""
|
||||
if [ "$BUMP_PATCH" = true ]; then
|
||||
BACKEND_VERSION_CURRENT=$(read_backend_version)
|
||||
FRONTEND_VERSION_CURRENT=$(read_frontend_version)
|
||||
|
||||
# Auto-commit if requested
|
||||
if [ "$AUTO_COMMIT" = true ]; then
|
||||
echo "Auto-committing changes..."
|
||||
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"
|
||||
if [ -z "$BACKEND_VERSION_CURRENT" ] && [ -z "$FRONTEND_VERSION_CURRENT" ]; then
|
||||
echo "❌ Cannot bump: version files missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
echo "Next step:"
|
||||
echo " Push: git push && git push --tags"
|
||||
else
|
||||
echo "Next steps:"
|
||||
echo "1. Review changes: git diff"
|
||||
if [ "$BACKEND_VERSION" = "$FRONTEND_VERSION" ]; then
|
||||
echo "2. Commit changes: git commit -am 'Bump version to $BACKEND_VERSION'"
|
||||
echo "3. Tag release: git tag v$BACKEND_VERSION"
|
||||
git tag v$BACKEND_VERSION
|
||||
else
|
||||
echo "2. Commit changes: git commit -am 'Bump backend to $BACKEND_VERSION, frontend to $FRONTEND_VERSION'"
|
||||
echo "3. Tag releases: git tag backend-v$BACKEND_VERSION && git tag frontend-v$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"
|
||||
|
||||
if [ -z "$BACKEND_VERSION_ARG" ] && [ -n "$BACKEND_VERSION_CURRENT" ]; then
|
||||
NEXT_BACKEND_VERSION=$(bump_patch "$BACKEND_VERSION_CURRENT")
|
||||
if [ -z "$NEXT_BACKEND_VERSION" ]; then
|
||||
echo "❌ Cannot bump backend: invalid version format '$BACKEND_VERSION_CURRENT'" >&2
|
||||
exit 1
|
||||
fi
|
||||
BACKEND_VERSION_ARG="$NEXT_BACKEND_VERSION"
|
||||
echo "✓ Bumping backend version to $BACKEND_VERSION_ARG"
|
||||
fi
|
||||
|
||||
if [ -z "$FRONTEND_VERSION_ARG" ] && [ -n "$FRONTEND_VERSION_CURRENT" ]; then
|
||||
NEXT_FRONTEND_VERSION=$(bump_patch "$FRONTEND_VERSION_CURRENT")
|
||||
if [ -z "$NEXT_FRONTEND_VERSION" ]; then
|
||||
echo "❌ Cannot bump frontend: invalid version format '$FRONTEND_VERSION_CURRENT'" >&2
|
||||
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
|
||||
echo "4. Push: git push && git push --tags"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user