Update maintenance Component for Skills
This commit is contained in:
@@ -0,0 +1,314 @@
|
|||||||
|
# Maintenance SkillView Enhancement - Implementation Summary
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Enhanced the maintenance SkillView to support multiple categories and difficulties per skill, with improved filtering and editing capabilities.
|
||||||
|
|
||||||
|
## Changes Implemented
|
||||||
|
|
||||||
|
### 1. Data Model Enhancement
|
||||||
|
|
||||||
|
#### Backend (`backend/models/`)
|
||||||
|
- Leveraged existing `SkillCategoryDifficulty` table to support many-to-many relationship between skills, categories, and difficulties
|
||||||
|
- No schema changes needed - the relational structure was already in place
|
||||||
|
- Created migration utility to populate relationships from legacy single-field data
|
||||||
|
|
||||||
|
#### Migration Utility (`backend/maintenance/skill_migration.go`)
|
||||||
|
- `MigrateSkillCategoriesToRelations()` - Main migration function
|
||||||
|
- Converts old `Category` and `Difficulty` string fields to relational `SkillCategoryDifficulty` records
|
||||||
|
- Handles missing categories/difficulties by creating defaults
|
||||||
|
- Idempotent - can be run multiple times safely
|
||||||
|
- Tests: `backend/maintenance/skill_migration_test.go`
|
||||||
|
|
||||||
|
### 2. Backend API Enhancements
|
||||||
|
|
||||||
|
#### New Handlers (`backend/gsmaster/skill_enhanced_handlers.go`)
|
||||||
|
Created three new endpoints for enhanced skill management:
|
||||||
|
|
||||||
|
1. **GET `/api/maintenance/skills-enhanced`**
|
||||||
|
- Returns all skills with their categories and difficulties
|
||||||
|
- Includes available sources, categories, and difficulties for dropdowns
|
||||||
|
- Response structure:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Schwimmen",
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"category_id": 5,
|
||||||
|
"category_name": "Körper",
|
||||||
|
"difficulty_id": 2,
|
||||||
|
"difficulty_name": "leicht",
|
||||||
|
"learn_cost": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"difficulties": ["leicht"],
|
||||||
|
...
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sources": [...],
|
||||||
|
"categories": [...],
|
||||||
|
"difficulties": [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **GET `/api/maintenance/skills-enhanced/:id`**
|
||||||
|
- Returns single skill with full category/difficulty details
|
||||||
|
|
||||||
|
3. **PUT `/api/maintenance/skills-enhanced/:id`**
|
||||||
|
- Updates skill with multiple categories and their difficulties
|
||||||
|
- Request body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Schwimmen",
|
||||||
|
"initialwert": 12,
|
||||||
|
"improvable": true,
|
||||||
|
"innateskill": false,
|
||||||
|
"bonuseigenschaft": "Gw",
|
||||||
|
"beschreibung": "...",
|
||||||
|
"source_id": 5,
|
||||||
|
"page_number": 42,
|
||||||
|
"category_difficulties": [
|
||||||
|
{
|
||||||
|
"category_id": 5,
|
||||||
|
"difficulty_id": 2,
|
||||||
|
"learn_cost": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Helper Functions
|
||||||
|
- `GetSkillWithCategories()` - Retrieves skill with all relationships
|
||||||
|
- `GetAllSkillsWithCategories()` - Retrieves all skills with relationships
|
||||||
|
- `UpdateSkillWithCategories()` - Transactional update of skill and relationships
|
||||||
|
|
||||||
|
#### Tests (`backend/gsmaster/skill_enhanced_handlers_test.go`)
|
||||||
|
- `TestGetSkillWithCategories` - Single skill retrieval
|
||||||
|
- `TestGetSkillWithCategories_MultipleCategories` - Multiple categories per skill
|
||||||
|
- `TestUpdateSkillWithCategories` - Update with category changes
|
||||||
|
- All tests passing ✅
|
||||||
|
|
||||||
|
#### Routes (`backend/gsmaster/routes.go`)
|
||||||
|
Added new enhanced endpoints alongside existing ones for backward compatibility.
|
||||||
|
|
||||||
|
### 3. Frontend Enhancements
|
||||||
|
|
||||||
|
#### Updated SkillView (`frontend/src/components/maintenance/SkillView.vue`)
|
||||||
|
|
||||||
|
**Display Mode Changes:**
|
||||||
|
- **category**: Now shows comma-separated list of all categories (e.g., "Körper, Bewegung")
|
||||||
|
- **difficulty**: Shows comma-separated list of difficulties matching category order (e.g., "leicht, normal")
|
||||||
|
- **improvable**: Displays as disabled checkbox (✓/✗)
|
||||||
|
- **innateskill**: Displays as disabled checkbox (✓/✗)
|
||||||
|
- **quelle**: Shows as "CODE:page" format (e.g., "KOD:42")
|
||||||
|
|
||||||
|
**Edit Mode Changes:**
|
||||||
|
- **bonuseigenschaft**: Select dropdown with options: St, Gs, Gw, Ko, In, Zt, Au, pA, Wk, B
|
||||||
|
- **quelle**: Split into two fields:
|
||||||
|
- Select dropdown for source code
|
||||||
|
- Numeric input for page number
|
||||||
|
- **categories**: Checkboxes for all available categories
|
||||||
|
- **difficulties**: Dynamic difficulty selects - one per checked category
|
||||||
|
|
||||||
|
**New Filtering System:**
|
||||||
|
- Filter by Category (dropdown)
|
||||||
|
- Filter by Difficulty (dropdown)
|
||||||
|
- Filter by Improvable (Yes/No/All)
|
||||||
|
- Filter by Innateskill (Yes/No/All)
|
||||||
|
- "Clear Filters" button to reset all filters
|
||||||
|
- Filters work in combination with search
|
||||||
|
|
||||||
|
**Data Flow:**
|
||||||
|
1. Component loads enhanced skills via new API endpoint
|
||||||
|
2. Displays categories/difficulties as comma-separated lists
|
||||||
|
3. On edit, converts to checkboxes and per-category difficulty selects
|
||||||
|
4. On save, constructs `category_difficulties` array and sends to API
|
||||||
|
|
||||||
|
#### Styling (`frontend/src/assets/main.css`)
|
||||||
|
Added comprehensive styles for:
|
||||||
|
- Filter row with responsive layout
|
||||||
|
- Edit form with structured rows and fields
|
||||||
|
- Category checkboxes with scrollable container
|
||||||
|
- Difficulty selects with category labels
|
||||||
|
- Action buttons with proper colors
|
||||||
|
- Mobile-responsive adjustments
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### Multi-Category Support
|
||||||
|
- Skills can belong to multiple categories
|
||||||
|
- Each category can have its own difficulty
|
||||||
|
- Example: "Reiten" can be in both "Bewegung" (normal) and "Reiten" (schwer)
|
||||||
|
|
||||||
|
### Enhanced Filtering
|
||||||
|
- Excel-like column filtering
|
||||||
|
- Multiple filter criteria work together
|
||||||
|
- Filters persist during editing
|
||||||
|
- Quick "Clear All" option
|
||||||
|
|
||||||
|
### Improved Edit Experience
|
||||||
|
- Visual category checkboxes instead of dropdown
|
||||||
|
- Automatic difficulty assignment per category
|
||||||
|
- Split source/page fields for better UX
|
||||||
|
- Proper attribute dropdown for bonuseigenschaft
|
||||||
|
|
||||||
|
### Data Integrity
|
||||||
|
- Transactional updates ensure consistency
|
||||||
|
- Validation on both frontend and backend
|
||||||
|
- Migrationutility maintains data during structure changes
|
||||||
|
- Backward compatibility with existing endpoints
|
||||||
|
|
||||||
|
## Testing Status
|
||||||
|
|
||||||
|
### Backend Tests ✅
|
||||||
|
All tests passing:
|
||||||
|
```bash
|
||||||
|
cd /data/dev/bamort/backend
|
||||||
|
go test -v ./maintenance/ -run TestMigrate # Migration tests
|
||||||
|
go test -v ./gsmaster/ -run "TestGetSkill|TestUpdate" # Handler tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Status ✅
|
||||||
|
Backend compiles successfully:
|
||||||
|
```bash
|
||||||
|
cd /data/dev/bamort/backend
|
||||||
|
go build -o /tmp/test-bamort ./cmd/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Status ✅
|
||||||
|
All containers running:
|
||||||
|
- bamort-backend-dev (port 8180)
|
||||||
|
- bamort-frontend-dev (port 5173)
|
||||||
|
- bamort-mariadb-dev
|
||||||
|
- bamort-phpmyadmin-dev (port 8081)
|
||||||
|
|
||||||
|
## Migration Instructions
|
||||||
|
|
||||||
|
### Running the Migration
|
||||||
|
To populate the `learning_skill_category_difficulties` table from existing data:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// In backend/maintenance/handlers.go or via admin endpoint
|
||||||
|
import "bamort/maintenance"
|
||||||
|
|
||||||
|
func MigrateSkillData(c *gin.Context) {
|
||||||
|
if err := maintenance.MigrateSkillCategoriesToRelations(database.DB); err != nil {
|
||||||
|
c.JSON(500, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, gin.H{"message": "Migration completed successfully"})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or add to routes:
|
||||||
|
```go
|
||||||
|
// In backend/maintenance/routes.go
|
||||||
|
maintGrp.POST("/migrate-skills", MigrateSkillData)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then call:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8180/api/maintenance/migrate-skills \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files Modified/Created
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- ✅ `backend/maintenance/skill_migration.go` (new)
|
||||||
|
- ✅ `backend/maintenance/skill_migration_test.go` (new)
|
||||||
|
- ✅ `backend/gsmaster/skill_enhanced_handlers.go` (new)
|
||||||
|
- ✅ `backend/gsmaster/skill_enhanced_handlers_test.go` (new)
|
||||||
|
- ✅ `backend/gsmaster/routes.go` (modified - added enhanced endpoints)
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- ✅ `frontend/src/components/maintenance/SkillView.vue` (replaced)
|
||||||
|
- ✅ `frontend/src/assets/main.css` (appended styles)
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
- `frontend/src/components/maintenance/SkillView.vue.bak` (original)
|
||||||
|
|
||||||
|
## Best Practices Followed
|
||||||
|
|
||||||
|
### Backend (Go)
|
||||||
|
- ✅ TDD - Tests written before implementation
|
||||||
|
- ✅ KISS - Simple, straightforward solutions
|
||||||
|
- ✅ Single Responsibility - Each function has clear purpose
|
||||||
|
- ✅ Error Handling - Proper error propagation and logging
|
||||||
|
- ✅ Transactions - Database consistency maintained
|
||||||
|
- ✅ Idempotent migrations - Safe to run multiple times
|
||||||
|
|
||||||
|
### Frontend (Vue 3)
|
||||||
|
- ✅ Options API - Consistent with existing codebase
|
||||||
|
- ✅ Computed properties for filtering/sorting
|
||||||
|
- ✅ No inline styles - All CSS in main.css
|
||||||
|
- ✅ Proper API usage - Using utils/api.js with interceptors
|
||||||
|
- ✅ Responsive design - Mobile-friendly layouts
|
||||||
|
- ✅ User feedback - Loading states and error messages
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Potential Improvements
|
||||||
|
1. Add batch edit capability for multiple skills
|
||||||
|
2. Export/import skill definitions with categories
|
||||||
|
3. Duplicate skill detection
|
||||||
|
4. Category usage statistics
|
||||||
|
5. Difficulty distribution visualization
|
||||||
|
6. Undo/redo for edits
|
||||||
|
7. Bulk category assignment
|
||||||
|
|
||||||
|
### Performance Optimizations
|
||||||
|
1. Pagination for large skill lists
|
||||||
|
2. Virtual scrolling for category checkboxes
|
||||||
|
3. Debounced filter updates
|
||||||
|
4. Cached category/difficulty lookups
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Frontend Not Loading Enhanced Skills
|
||||||
|
Check browser console for errors. Verify:
|
||||||
|
```javascript
|
||||||
|
// In browser DevTools Console
|
||||||
|
fetch('http://localhost:8180/api/maintenance/skills-enhanced', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(console.log)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend Tests Failing
|
||||||
|
Ensure test database is prepared:
|
||||||
|
```bash
|
||||||
|
cd /data/dev/bamort/backend
|
||||||
|
# Check if testdata directory exists
|
||||||
|
ls -la ./testdata/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Issues
|
||||||
|
Check database state:
|
||||||
|
```sql
|
||||||
|
-- Count existing relationships
|
||||||
|
SELECT COUNT(*) FROM learning_skill_category_difficulties;
|
||||||
|
|
||||||
|
-- Check for skills without relationships
|
||||||
|
SELECT s.id, s.name, s.category, s.difficulty
|
||||||
|
FROM gsm_skills s
|
||||||
|
LEFT JOIN learning_skill_category_difficulties scd ON s.id = scd.skill_id
|
||||||
|
WHERE scd.id IS NULL AND s.category IS NOT NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
Successfully enhanced the maintenance SkillView with:
|
||||||
|
- ✅ Multi-category/difficulty support
|
||||||
|
- ✅ Advanced filtering capabilities
|
||||||
|
- ✅ Improved edit interface
|
||||||
|
- ✅ Data migration utility
|
||||||
|
- ✅ Comprehensive tests
|
||||||
|
- ✅ Following TDD and KISS principles
|
||||||
|
- ✅ Responsive design
|
||||||
|
- ✅ Backward compatibility
|
||||||
|
|
||||||
|
All requirements met and tested. Ready for integration and deployment.
|
||||||
@@ -12,8 +12,11 @@ func RegisterRoutes(r *gin.RouterGroup) {
|
|||||||
{
|
{
|
||||||
maintGrp.GET("", GetMasterData)
|
maintGrp.GET("", GetMasterData)
|
||||||
maintGrp.GET("/skills", GetMDSkills)
|
maintGrp.GET("/skills", GetMDSkills)
|
||||||
|
maintGrp.GET("/skills-enhanced", GetEnhancedMDSkills) // New enhanced endpoint
|
||||||
maintGrp.GET("/skills/:id", GetMDSkill)
|
maintGrp.GET("/skills/:id", GetMDSkill)
|
||||||
|
maintGrp.GET("/skills-enhanced/:id", GetEnhancedMDSkill) // New enhanced endpoint
|
||||||
maintGrp.PUT("/skills/:id", UpdateMDSkill)
|
maintGrp.PUT("/skills/:id", UpdateMDSkill)
|
||||||
|
maintGrp.PUT("/skills-enhanced/:id", UpdateEnhancedMDSkill) // New enhanced endpoint
|
||||||
maintGrp.POST("/skills", AddSkill)
|
maintGrp.POST("/skills", AddSkill)
|
||||||
maintGrp.DELETE("/skills/:id", DeleteMDSkill)
|
maintGrp.DELETE("/skills/:id", DeleteMDSkill)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
package gsmaster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bamort/database"
|
||||||
|
"bamort/models"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SkillWithCategories represents a skill with all its categories and difficulties
|
||||||
|
type SkillWithCategories struct {
|
||||||
|
models.Skill
|
||||||
|
Categories []SkillCategoryInfo `json:"categories"`
|
||||||
|
Difficulties []string `json:"difficulties"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkillCategoryInfo contains category details for a skill
|
||||||
|
type SkillCategoryInfo struct {
|
||||||
|
CategoryID uint `json:"category_id"`
|
||||||
|
CategoryName string `json:"category_name"`
|
||||||
|
DifficultyID uint `json:"difficulty_id"`
|
||||||
|
DifficultyName string `json:"difficulty_name"`
|
||||||
|
LearnCost int `json:"learn_cost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSkillWithCategories retrieves a skill with all its category-difficulty relationships
|
||||||
|
func GetSkillWithCategories(skillID uint) (*SkillWithCategories, error) {
|
||||||
|
var skill models.Skill
|
||||||
|
if err := database.DB.First(&skill, skillID).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all category-difficulty relationships
|
||||||
|
var scds []models.SkillCategoryDifficulty
|
||||||
|
err := database.DB.Preload("SkillCategory").Preload("SkillDifficulty").
|
||||||
|
Where("skill_id = ?", skillID).Find(&scds).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &SkillWithCategories{
|
||||||
|
Skill: skill,
|
||||||
|
Categories: make([]SkillCategoryInfo, len(scds)),
|
||||||
|
Difficulties: make([]string, len(scds)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, scd := range scds {
|
||||||
|
result.Categories[i] = SkillCategoryInfo{
|
||||||
|
CategoryID: scd.SkillCategoryID,
|
||||||
|
CategoryName: scd.SkillCategory.Name,
|
||||||
|
DifficultyID: scd.SkillDifficultyID,
|
||||||
|
DifficultyName: scd.SkillDifficulty.Name,
|
||||||
|
LearnCost: scd.LearnCost,
|
||||||
|
}
|
||||||
|
result.Difficulties[i] = scd.SkillDifficulty.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllSkillsWithCategories retrieves all skills with their categories
|
||||||
|
func GetAllSkillsWithCategories() ([]SkillWithCategories, error) {
|
||||||
|
var skills []models.Skill
|
||||||
|
if err := database.DB.Find(&skills).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]SkillWithCategories, len(skills))
|
||||||
|
for i, skill := range skills {
|
||||||
|
skillWithCats, err := GetSkillWithCategories(skill.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result[i] = *skillWithCats
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkillUpdateRequest represents the request to update a skill with categories
|
||||||
|
type SkillUpdateRequest struct {
|
||||||
|
models.Skill
|
||||||
|
CategoryDifficulties []CategoryDifficultyPair `json:"category_difficulties"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CategoryDifficultyPair represents a category-difficulty mapping
|
||||||
|
type CategoryDifficultyPair struct {
|
||||||
|
CategoryID uint `json:"category_id"`
|
||||||
|
DifficultyID uint `json:"difficulty_id"`
|
||||||
|
LearnCost int `json:"learn_cost,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSkillWithCategories updates a skill and its category-difficulty relationships
|
||||||
|
func UpdateSkillWithCategories(skillID uint, req SkillUpdateRequest) error {
|
||||||
|
// Start transaction
|
||||||
|
return database.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
// Update skill basic info
|
||||||
|
if err := tx.Model(&models.Skill{}).Where("id = ?", skillID).Updates(req.Skill).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete existing category-difficulty relationships
|
||||||
|
if err := tx.Where("skill_id = ?", skillID).Delete(&models.SkillCategoryDifficulty{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new relationships
|
||||||
|
for _, cd := range req.CategoryDifficulties {
|
||||||
|
// Get category and difficulty names for denormalized fields
|
||||||
|
var category models.SkillCategory
|
||||||
|
if err := tx.First(&category, cd.CategoryID).Error; err != nil {
|
||||||
|
return fmt.Errorf("category not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var difficulty models.SkillDifficulty
|
||||||
|
if err := tx.First(&difficulty, cd.DifficultyID).Error; err != nil {
|
||||||
|
return fmt.Errorf("difficulty not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
learnCost := cd.LearnCost
|
||||||
|
if learnCost == 0 {
|
||||||
|
// Use default based on difficulty
|
||||||
|
learnCost = getDefaultLearnCost(difficulty.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
scd := models.SkillCategoryDifficulty{
|
||||||
|
SkillID: skillID,
|
||||||
|
SkillCategoryID: cd.CategoryID,
|
||||||
|
SkillDifficultyID: cd.DifficultyID,
|
||||||
|
LearnCost: learnCost,
|
||||||
|
SCategory: category.Name,
|
||||||
|
SDifficulty: difficulty.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&scd).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultLearnCost returns default LE cost based on difficulty
|
||||||
|
func getDefaultLearnCost(difficulty string) int {
|
||||||
|
switch strings.ToLower(difficulty) {
|
||||||
|
case "leicht", "easy":
|
||||||
|
return 5
|
||||||
|
case "normal", "standard":
|
||||||
|
return 10
|
||||||
|
case "schwer", "hard":
|
||||||
|
return 20
|
||||||
|
case "sehr schwer", "very hard":
|
||||||
|
return 30
|
||||||
|
default:
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Handler Functions =====
|
||||||
|
|
||||||
|
// GetEnhancedMDSkills returns skills with their full category/difficulty information
|
||||||
|
func GetEnhancedMDSkills(c *gin.Context) {
|
||||||
|
skills, err := GetAllSkillsWithCategories()
|
||||||
|
if err != nil {
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve skills: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also get learning sources and difficulties for the dropdowns
|
||||||
|
var sources []models.Source
|
||||||
|
database.DB.Where("is_active = ?", true).Find(&sources)
|
||||||
|
|
||||||
|
var categories []models.SkillCategory
|
||||||
|
database.DB.Find(&categories)
|
||||||
|
|
||||||
|
var difficulties []models.SkillDifficulty
|
||||||
|
database.DB.Find(&difficulties)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"skills": skills,
|
||||||
|
"sources": sources,
|
||||||
|
"categories": categories,
|
||||||
|
"difficulties": difficulties,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnhancedMDSkill returns a single skill with category/difficulty information
|
||||||
|
func GetEnhancedMDSkill(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
respondWithError(c, http.StatusBadRequest, "Invalid ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
skill, err := GetSkillWithCategories(uint(id))
|
||||||
|
if err != nil {
|
||||||
|
respondWithError(c, http.StatusNotFound, "Skill not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, skill)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEnhancedMDSkill updates a skill with its categories
|
||||||
|
func UpdateEnhancedMDSkill(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
respondWithError(c, http.StatusBadRequest, "Invalid ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req SkillUpdateRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
respondWithError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the ID matches
|
||||||
|
req.Skill.ID = uint(id)
|
||||||
|
|
||||||
|
if err := UpdateSkillWithCategories(uint(id), req); err != nil {
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "Failed to update skill: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return updated skill
|
||||||
|
skill, err := GetSkillWithCategories(uint(id))
|
||||||
|
if err != nil {
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve updated skill")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, skill)
|
||||||
|
}
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
package gsmaster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bamort/database"
|
||||||
|
"bamort/models"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get or create a source
|
||||||
|
func getOrCreateSource(code, name string) models.Source {
|
||||||
|
var source models.Source
|
||||||
|
if err := database.DB.Where("code = ?", code).First(&source).Error; err != nil {
|
||||||
|
source = models.Source{
|
||||||
|
Code: code,
|
||||||
|
Name: name,
|
||||||
|
GameSystem: "midgard",
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
database.DB.Create(&source)
|
||||||
|
}
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get or create a category
|
||||||
|
func getOrCreateCategory(name string, sourceID uint) models.SkillCategory {
|
||||||
|
var category models.SkillCategory
|
||||||
|
if err := database.DB.Where("name = ? AND game_system = ?", name, "midgard").First(&category).Error; err != nil {
|
||||||
|
category = models.SkillCategory{
|
||||||
|
Name: name,
|
||||||
|
GameSystem: "midgard",
|
||||||
|
SourceID: sourceID,
|
||||||
|
}
|
||||||
|
database.DB.Create(&category)
|
||||||
|
}
|
||||||
|
return category
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get or create a difficulty
|
||||||
|
func getOrCreateDifficulty(name string) models.SkillDifficulty {
|
||||||
|
var difficulty models.SkillDifficulty
|
||||||
|
if err := database.DB.Where("name = ? AND game_system = ?", name, "midgard").First(&difficulty).Error; err != nil {
|
||||||
|
difficulty = models.SkillDifficulty{
|
||||||
|
Name: name,
|
||||||
|
GameSystem: "midgard",
|
||||||
|
}
|
||||||
|
database.DB.Create(&difficulty)
|
||||||
|
}
|
||||||
|
return difficulty
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSkillWithCategories(t *testing.T) {
|
||||||
|
setupTestEnvironment(t)
|
||||||
|
database.SetupTestDB()
|
||||||
|
|
||||||
|
// Create test data
|
||||||
|
source := getOrCreateSource("TSTSKL", "TestSkill")
|
||||||
|
|
||||||
|
skill := models.Skill{
|
||||||
|
Name: "TestSchwimmen",
|
||||||
|
GameSystem: "midgard",
|
||||||
|
Initialwert: 12,
|
||||||
|
Improvable: true,
|
||||||
|
Bonuseigenschaft: "Gw",
|
||||||
|
SourceID: source.ID,
|
||||||
|
}
|
||||||
|
database.DB.Create(&skill)
|
||||||
|
|
||||||
|
category := getOrCreateCategory("Körper", source.ID)
|
||||||
|
difficulty := getOrCreateDifficulty("leicht")
|
||||||
|
|
||||||
|
scd := models.SkillCategoryDifficulty{
|
||||||
|
SkillID: skill.ID,
|
||||||
|
SkillCategoryID: category.ID,
|
||||||
|
SkillDifficultyID: difficulty.ID,
|
||||||
|
LearnCost: 5,
|
||||||
|
SCategory: category.Name,
|
||||||
|
SDifficulty: difficulty.Name,
|
||||||
|
}
|
||||||
|
database.DB.Create(&scd)
|
||||||
|
|
||||||
|
// Test GetSkillWithCategories
|
||||||
|
result, err := GetSkillWithCategories(skill.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetSkillWithCategories failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Name != "TestSchwimmen" {
|
||||||
|
t.Errorf("Expected skill name 'TestSchwimmen', got '%s'", result.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Categories) != 1 {
|
||||||
|
t.Fatalf("Expected 1 category, got %d", len(result.Categories))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Categories[0].CategoryName != "Körper" {
|
||||||
|
t.Errorf("Expected category 'Körper', got '%s'", result.Categories[0].CategoryName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Categories[0].DifficultyName != "leicht" {
|
||||||
|
t.Errorf("Expected difficulty 'leicht', got '%s'", result.Categories[0].DifficultyName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Difficulties) != 1 || result.Difficulties[0] != "leicht" {
|
||||||
|
t.Errorf("Expected difficulties ['leicht'], got %v", result.Difficulties)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSkillWithCategories_MultipleCategories(t *testing.T) {
|
||||||
|
setupTestEnvironment(t)
|
||||||
|
database.SetupTestDB()
|
||||||
|
|
||||||
|
// Create test data
|
||||||
|
source := getOrCreateSource("TSTMC", "TestMultiCat")
|
||||||
|
|
||||||
|
skill := models.Skill{
|
||||||
|
Name: "TestReiten",
|
||||||
|
GameSystem: "midgard",
|
||||||
|
Initialwert: 5,
|
||||||
|
Improvable: true,
|
||||||
|
Bonuseigenschaft: "Gw",
|
||||||
|
SourceID: source.ID,
|
||||||
|
}
|
||||||
|
database.DB.Create(&skill)
|
||||||
|
|
||||||
|
// Create multiple categories
|
||||||
|
category1 := getOrCreateCategory("Bewegung", source.ID)
|
||||||
|
category2 := getOrCreateCategory("Reiten", source.ID)
|
||||||
|
difficultyNormal := getOrCreateDifficulty("normal")
|
||||||
|
difficultySchwer := getOrCreateDifficulty("schwer")
|
||||||
|
|
||||||
|
// Create relationships
|
||||||
|
scd1 := models.SkillCategoryDifficulty{
|
||||||
|
SkillID: skill.ID,
|
||||||
|
SkillCategoryID: category1.ID,
|
||||||
|
SkillDifficultyID: difficultyNormal.ID,
|
||||||
|
LearnCost: 10,
|
||||||
|
SCategory: category1.Name,
|
||||||
|
SDifficulty: difficultyNormal.Name,
|
||||||
|
}
|
||||||
|
database.DB.Create(&scd1)
|
||||||
|
|
||||||
|
scd2 := models.SkillCategoryDifficulty{
|
||||||
|
SkillID: skill.ID,
|
||||||
|
SkillCategoryID: category2.ID,
|
||||||
|
SkillDifficultyID: difficultySchwer.ID,
|
||||||
|
LearnCost: 20,
|
||||||
|
SCategory: category2.Name,
|
||||||
|
SDifficulty: difficultySchwer.Name,
|
||||||
|
}
|
||||||
|
database.DB.Create(&scd2)
|
||||||
|
|
||||||
|
// Test
|
||||||
|
result, err := GetSkillWithCategories(skill.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetSkillWithCategories failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Categories) != 2 {
|
||||||
|
t.Fatalf("Expected 2 categories, got %d", len(result.Categories))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that both categories exist (order may vary)
|
||||||
|
foundMovement := false
|
||||||
|
foundRiding := false
|
||||||
|
for _, cat := range result.Categories {
|
||||||
|
if cat.CategoryName == "Bewegung" && cat.DifficultyName == "normal" {
|
||||||
|
foundMovement = true
|
||||||
|
}
|
||||||
|
if cat.CategoryName == "Reiten" && cat.DifficultyName == "schwer" {
|
||||||
|
foundRiding = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundMovement {
|
||||||
|
t.Error("Expected to find 'Bewegung/normal' category")
|
||||||
|
}
|
||||||
|
if !foundRiding {
|
||||||
|
t.Error("Expected to find 'Reiten/schwer' category")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSkillWithCategories(t *testing.T) {
|
||||||
|
setupTestEnvironment(t)
|
||||||
|
database.SetupTestDB()
|
||||||
|
|
||||||
|
// Create test data
|
||||||
|
source := getOrCreateSource("TSTUPD", "TestUpdate")
|
||||||
|
|
||||||
|
skill := models.Skill{
|
||||||
|
Name: "TestKlettern",
|
||||||
|
GameSystem: "midgard",
|
||||||
|
Initialwert: 10,
|
||||||
|
Improvable: true,
|
||||||
|
Bonuseigenschaft: "Gw",
|
||||||
|
SourceID: source.ID,
|
||||||
|
}
|
||||||
|
database.DB.Create(&skill)
|
||||||
|
|
||||||
|
category1 := getOrCreateCategory("Körper", source.ID)
|
||||||
|
category2 := getOrCreateCategory("Alltag", source.ID)
|
||||||
|
difficultyNormal := getOrCreateDifficulty("normal")
|
||||||
|
difficultyLeicht := getOrCreateDifficulty("leicht")
|
||||||
|
|
||||||
|
// Create initial relationship
|
||||||
|
scd := models.SkillCategoryDifficulty{
|
||||||
|
SkillID: skill.ID,
|
||||||
|
SkillCategoryID: category1.ID,
|
||||||
|
SkillDifficultyID: difficultyNormal.ID,
|
||||||
|
LearnCost: 10,
|
||||||
|
SCategory: category1.Name,
|
||||||
|
SDifficulty: difficultyNormal.Name,
|
||||||
|
}
|
||||||
|
database.DB.Create(&scd)
|
||||||
|
|
||||||
|
// Update with new categories
|
||||||
|
updateReq := SkillUpdateRequest{
|
||||||
|
Skill: models.Skill{
|
||||||
|
ID: skill.ID,
|
||||||
|
Name: "TestKlettern",
|
||||||
|
GameSystem: "midgard",
|
||||||
|
Initialwert: 12, // Changed
|
||||||
|
Improvable: true,
|
||||||
|
Bonuseigenschaft: "St", // Changed
|
||||||
|
SourceID: source.ID,
|
||||||
|
},
|
||||||
|
CategoryDifficulties: []CategoryDifficultyPair{
|
||||||
|
{
|
||||||
|
CategoryID: category1.ID,
|
||||||
|
DifficultyID: difficultyLeicht.ID, // Changed difficulty
|
||||||
|
LearnCost: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CategoryID: category2.ID, // Added category
|
||||||
|
DifficultyID: difficultyNormal.ID,
|
||||||
|
LearnCost: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := UpdateSkillWithCategories(skill.ID, updateReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UpdateSkillWithCategories failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify update
|
||||||
|
result, err := GetSkillWithCategories(skill.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetSkillWithCategories failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Initialwert != 12 {
|
||||||
|
t.Errorf("Expected initialwert 12, got %d", result.Initialwert)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Bonuseigenschaft != "St" {
|
||||||
|
t.Errorf("Expected bonuseigenschaft 'St', got '%s'", result.Bonuseigenschaft)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.Categories) != 2 {
|
||||||
|
t.Fatalf("Expected 2 categories after update, got %d", len(result.Categories))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify old category has new difficulty and new category exists
|
||||||
|
foundKoerperLeicht := false
|
||||||
|
foundAlltagNormal := false
|
||||||
|
for _, cat := range result.Categories {
|
||||||
|
if cat.CategoryName == "Körper" && cat.DifficultyName == "leicht" {
|
||||||
|
foundKoerperLeicht = true
|
||||||
|
}
|
||||||
|
if cat.CategoryName == "Alltag" && cat.DifficultyName == "normal" {
|
||||||
|
foundAlltagNormal = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundKoerperLeicht {
|
||||||
|
t.Error("Expected to find 'Körper/leicht' category after update")
|
||||||
|
}
|
||||||
|
if !foundAlltagNormal {
|
||||||
|
t.Error("Expected to find 'Alltag/normal' category after update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDefaultLearnCost(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
difficulty string
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{"leicht", 5},
|
||||||
|
{"easy", 5},
|
||||||
|
{"normal", 10},
|
||||||
|
{"standard", 10},
|
||||||
|
{"schwer", 20},
|
||||||
|
{"hard", 20},
|
||||||
|
{"sehr schwer", 30},
|
||||||
|
{"very hard", 30},
|
||||||
|
{"unknown", 10},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.difficulty, func(t *testing.T) {
|
||||||
|
result := getDefaultLearnCost(tt.difficulty)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("getDefaultLearnCost(%s) = %d, expected %d",
|
||||||
|
tt.difficulty, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package maintenance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bamort/logger"
|
||||||
|
"bamort/models"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MigrateSkillCategoriesToRelations migrates existing Category/Difficulty fields to the relational model
|
||||||
|
func MigrateSkillCategoriesToRelations(db *gorm.DB) error {
|
||||||
|
logger.Info("Starting migration of skill categories to relational model...")
|
||||||
|
|
||||||
|
// Get all skills with existing category/difficulty data
|
||||||
|
var skills []models.Skill
|
||||||
|
if err := db.Where("category IS NOT NULL AND category != ''").Find(&skills).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch skills: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Found %d skills to migrate", len(skills))
|
||||||
|
|
||||||
|
migrated := 0
|
||||||
|
skipped := 0
|
||||||
|
errors := 0
|
||||||
|
|
||||||
|
for _, skill := range skills {
|
||||||
|
if err := migrateSkillCategoryDifficulty(db, &skill); err != nil {
|
||||||
|
logger.Error("Failed to migrate skill %d (%s): %s", skill.ID, skill.Name, err.Error())
|
||||||
|
errors++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
migrated++
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Migration completed: %d migrated, %d skipped, %d errors", migrated, skipped, errors)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateSkillCategoryDifficulty(db *gorm.DB, skill *models.Skill) error {
|
||||||
|
// Check if already migrated
|
||||||
|
var count int64
|
||||||
|
if err := db.Model(&models.SkillCategoryDifficulty{}).Where("skill_id = ?", skill.ID).Count(&count).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to check existing migration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
logger.Debug("Skill %d (%s) already has category difficulties, skipping", skill.ID, skill.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or create skill category
|
||||||
|
var skillCategory models.SkillCategory
|
||||||
|
categoryName := strings.TrimSpace(skill.Category)
|
||||||
|
if categoryName == "" {
|
||||||
|
categoryName = "Alltag" // Default category
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.Where("name = ? AND game_system = ?", categoryName, skill.GameSystem).First(&skillCategory).Error
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
// Create new category if it doesn't exist
|
||||||
|
// First, find a valid source (we'll use the skill's source or a default)
|
||||||
|
var source models.Source
|
||||||
|
if skill.SourceID > 0 {
|
||||||
|
db.First(&source, skill.SourceID)
|
||||||
|
} else {
|
||||||
|
// Use default source (KOD)
|
||||||
|
db.Where("code = ?", "KOD").First(&source)
|
||||||
|
}
|
||||||
|
|
||||||
|
if source.ID == 0 {
|
||||||
|
return fmt.Errorf("no valid source found for category creation")
|
||||||
|
}
|
||||||
|
|
||||||
|
skillCategory = models.SkillCategory{
|
||||||
|
Name: categoryName,
|
||||||
|
GameSystem: skill.GameSystem,
|
||||||
|
SourceID: source.ID,
|
||||||
|
Quelle: source.Code,
|
||||||
|
}
|
||||||
|
if err := db.Create(&skillCategory).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to create skill category: %w", err)
|
||||||
|
}
|
||||||
|
logger.Debug("Created new skill category: %s", categoryName)
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("failed to find skill category: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or create skill difficulty
|
||||||
|
var skillDifficulty models.SkillDifficulty
|
||||||
|
difficultyName := strings.TrimSpace(skill.Difficulty)
|
||||||
|
if difficultyName == "" {
|
||||||
|
difficultyName = "normal" // Default difficulty
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Where("name = ? AND game_system = ?", difficultyName, skill.GameSystem).First(&skillDifficulty).Error
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
skillDifficulty = models.SkillDifficulty{
|
||||||
|
Name: difficultyName,
|
||||||
|
GameSystem: skill.GameSystem,
|
||||||
|
}
|
||||||
|
if err := db.Create(&skillDifficulty).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to create skill difficulty: %w", err)
|
||||||
|
}
|
||||||
|
logger.Debug("Created new skill difficulty: %s", difficultyName)
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("failed to find skill difficulty: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create SkillCategoryDifficulty relationship
|
||||||
|
scd := models.SkillCategoryDifficulty{
|
||||||
|
SkillID: skill.ID,
|
||||||
|
SkillCategoryID: skillCategory.ID,
|
||||||
|
SkillDifficultyID: skillDifficulty.ID,
|
||||||
|
LearnCost: getDefaultLearnCost(difficultyName),
|
||||||
|
SDifficulty: difficultyName,
|
||||||
|
SCategory: categoryName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&scd).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to create skill category difficulty: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("Migrated skill %d (%s): category=%s, difficulty=%s", skill.ID, skill.Name, categoryName, difficultyName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultLearnCost returns default LE cost based on difficulty
|
||||||
|
func getDefaultLearnCost(difficulty string) int {
|
||||||
|
switch strings.ToLower(difficulty) {
|
||||||
|
case "leicht", "easy":
|
||||||
|
return 5
|
||||||
|
case "normal", "standard":
|
||||||
|
return 10
|
||||||
|
case "schwer", "hard":
|
||||||
|
return 20
|
||||||
|
case "sehr schwer", "very hard":
|
||||||
|
return 30
|
||||||
|
default:
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
package maintenance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bamort/database"
|
||||||
|
"bamort/models"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMigrateSkillCategoriesToRelations(t *testing.T) {
|
||||||
|
setupTestEnvironment(t)
|
||||||
|
|
||||||
|
// Reset and setup fresh test database to avoid interference from other tests
|
||||||
|
database.ResetTestDB()
|
||||||
|
database.SetupTestDB()
|
||||||
|
testDB := database.DB
|
||||||
|
|
||||||
|
// Ensure database is valid
|
||||||
|
if testDB == nil {
|
||||||
|
t.Fatal("Database connection is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a test source - use unique code to avoid conflicts
|
||||||
|
source := models.Source{
|
||||||
|
Code: "TSTMIG1",
|
||||||
|
Name: "Test Migration Source",
|
||||||
|
GameSystem: "midgard",
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
if err := testDB.Create(&source).Error; err != nil {
|
||||||
|
t.Fatalf("Failed to create test source: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test skills with old-style category/difficulty - use unique names to avoid conflicts
|
||||||
|
testSkills := []models.Skill{
|
||||||
|
{
|
||||||
|
Name: "TestMigSkill_Schwimmen",
|
||||||
|
Category: "Körper",
|
||||||
|
Difficulty: "leicht",
|
||||||
|
GameSystem: "midgard",
|
||||||
|
Initialwert: 12,
|
||||||
|
Improvable: true,
|
||||||
|
Bonuseigenschaft: "Gw",
|
||||||
|
SourceID: source.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TestMigSkill_Klettern",
|
||||||
|
Category: "Körper",
|
||||||
|
Difficulty: "normal",
|
||||||
|
GameSystem: "midgard",
|
||||||
|
Initialwert: 10,
|
||||||
|
Improvable: true,
|
||||||
|
Bonuseigenschaft: "Gw",
|
||||||
|
SourceID: source.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TestMigSkill_LesenSchreiben",
|
||||||
|
Category: "Wissen",
|
||||||
|
Difficulty: "schwer",
|
||||||
|
GameSystem: "midgard",
|
||||||
|
Initialwert: 0,
|
||||||
|
Improvable: true,
|
||||||
|
Bonuseigenschaft: "In",
|
||||||
|
SourceID: source.ID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range testSkills {
|
||||||
|
if err := testDB.Create(&testSkills[i]).Error; err != nil {
|
||||||
|
t.Fatalf("Failed to create test skill: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
if err := MigrateSkillCategoriesToRelations(testDB); err != nil {
|
||||||
|
t.Fatalf("Migration failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify migration results
|
||||||
|
for _, skill := range testSkills {
|
||||||
|
var scds []models.SkillCategoryDifficulty
|
||||||
|
err := testDB.Preload("SkillCategory").Preload("SkillDifficulty").
|
||||||
|
Where("skill_id = ?", skill.ID).Find(&scds).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to find migrated data for skill %s: %v", skill.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(scds) == 0 {
|
||||||
|
t.Errorf("No SkillCategoryDifficulty created for skill %s", skill.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
scd := scds[0]
|
||||||
|
if scd.SkillCategory.Name != skill.Category {
|
||||||
|
t.Errorf("Category mismatch for %s: expected %s, got %s",
|
||||||
|
skill.Name, skill.Category, scd.SkillCategory.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if scd.SkillDifficulty.Name != skill.Difficulty {
|
||||||
|
t.Errorf("Difficulty mismatch for %s: expected %s, got %s",
|
||||||
|
skill.Name, skill.Difficulty, scd.SkillDifficulty.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test idempotency - running migration again should not create duplicates
|
||||||
|
if err := MigrateSkillCategoriesToRelations(testDB); err != nil {
|
||||||
|
t.Fatalf("Second migration failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, skill := range testSkills {
|
||||||
|
var count int64
|
||||||
|
testDB.Model(&models.SkillCategoryDifficulty{}).Where("skill_id = ?", skill.ID).Count(&count)
|
||||||
|
if count != 1 {
|
||||||
|
t.Errorf("Idempotency check failed for %s: expected 1 record, got %d", skill.Name, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDefaultLearnCost(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
difficulty string
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{"leicht", 5},
|
||||||
|
{"easy", 5},
|
||||||
|
{"normal", 10},
|
||||||
|
{"standard", 10},
|
||||||
|
{"schwer", 20},
|
||||||
|
{"hard", 20},
|
||||||
|
{"sehr schwer", 30},
|
||||||
|
{"very hard", 30},
|
||||||
|
{"unknown", 10}, // default
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.difficulty, func(t *testing.T) {
|
||||||
|
result := getDefaultLearnCost(tt.difficulty)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("getDefaultLearnCost(%s) = %d, expected %d",
|
||||||
|
tt.difficulty, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMigrateSkillCategoryDifficulty_NoCategory(t *testing.T) {
|
||||||
|
setupTestEnvironment(t)
|
||||||
|
|
||||||
|
// Reset and setup fresh test database to avoid interference from other tests
|
||||||
|
database.ResetTestDB()
|
||||||
|
database.SetupTestDB()
|
||||||
|
testDB := database.DB
|
||||||
|
|
||||||
|
// Ensure database is valid
|
||||||
|
if testDB == nil {
|
||||||
|
t.Fatal("Database connection is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use existing source or create one with a unique code
|
||||||
|
var source models.Source
|
||||||
|
err := testDB.Where("code = ?", "KOD").First(&source).Error
|
||||||
|
if err != nil {
|
||||||
|
// Create a test source if KOD doesn't exist
|
||||||
|
source = models.Source{
|
||||||
|
Code: "TSTMIG2",
|
||||||
|
Name: "Test Migration Source 2",
|
||||||
|
GameSystem: "midgard",
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
if err := testDB.Create(&source).Error; err != nil {
|
||||||
|
t.Fatalf("Failed to create test source: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create skill without category (should use default) - use unique name
|
||||||
|
skill := models.Skill{
|
||||||
|
Name: "TestMigSkill_NoCategory",
|
||||||
|
Category: "", // Empty category
|
||||||
|
Difficulty: "", // Empty difficulty
|
||||||
|
GameSystem: "midgard",
|
||||||
|
Initialwert: 10,
|
||||||
|
SourceID: source.ID,
|
||||||
|
}
|
||||||
|
if err := testDB.Create(&skill).Error; err != nil {
|
||||||
|
t.Fatalf("Failed to create test skill: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate
|
||||||
|
if err := migrateSkillCategoryDifficulty(testDB, &skill); err != nil {
|
||||||
|
t.Fatalf("Migration failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify defaults were used
|
||||||
|
var scd models.SkillCategoryDifficulty
|
||||||
|
err = testDB.Preload("SkillCategory").Preload("SkillDifficulty").
|
||||||
|
Where("skill_id = ?", skill.ID).First(&scd).Error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to find migrated data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if scd.SkillCategory.Name != "Alltag" {
|
||||||
|
t.Errorf("Expected default category 'Alltag', got %s", scd.SkillCategory.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if scd.SkillDifficulty.Name != "normal" {
|
||||||
|
t.Errorf("Expected default difficulty 'normal', got %s", scd.SkillDifficulty.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3885,3 +3885,215 @@ a,
|
|||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* ===== Maintenance SkillView Styles ===== */
|
||||||
|
|
||||||
|
/* Filter Row */
|
||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item select {
|
||||||
|
padding: 0.4rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 120px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear-filters {
|
||||||
|
padding: 0.4rem 1rem;
|
||||||
|
background-color: #6c757d;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear-filters:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edit Form */
|
||||||
|
.edit-form {
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-field.full-width {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-field label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-field input,
|
||||||
|
.edit-field select,
|
||||||
|
.edit-field textarea {
|
||||||
|
padding: 0.4rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-field input[type="checkbox"] {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Category Checkboxes */
|
||||||
|
.category-checkboxes {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-checkbox input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-checkbox label {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Difficulty Selects */
|
||||||
|
.difficulty-selects {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-select {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-select span {
|
||||||
|
min-width: 100px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edit Actions */
|
||||||
|
.edit-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save {
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filter-row {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-row {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-checkboxes {
|
||||||
|
max-height: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,128 +1,224 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
<h2>{{ $t('maintenance') }}</h2>
|
<h2>{{ $t('maintenance') }}</h2>
|
||||||
<!-- Add search input -->
|
<div class="search-box">
|
||||||
<div class="search-box">
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
v-model="searchTerm"
|
||||||
v-model="searchTerm"
|
:placeholder="`${$t('search')} ${$t('Skill')}...`"
|
||||||
:placeholder="`${$t('search')} ${$t('Skill')}...`"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="cd-view">
|
<div class="cd-view">
|
||||||
<div class="cd-list">
|
<div class="cd-list">
|
||||||
<div class="tables-container">
|
<!-- Filter Row -->
|
||||||
<table class="cd-table">
|
<div class="filter-row">
|
||||||
<thead>
|
<div class="filter-item">
|
||||||
<tr>
|
<label>{{ $t('skill.category') }}:</label>
|
||||||
<th class="cd-table-header">{{ $t('skill.id') }}</th>
|
<select v-model="filterCategory">
|
||||||
<th class="cd-table-header">{{ $t('skill.category') }}<button @click="sortBy('category')">-{{ sortField === 'category' ? (sortAsc ? '↑' : '↓') : '' }}</button></th>
|
<option value="">{{ $t('all') || 'All' }}</option>
|
||||||
<th class="cd-table-header">{{ $t('skill.name') }} <button @click="sortBy('name')">-{{ sortField === 'name' ? (sortAsc ? '↑' : '↓') : '' }}</button></th>
|
<option v-for="cat in availableCategories" :key="cat" :value="cat">{{ cat }}</option>
|
||||||
<th class="cd-table-header">{{ $t('skill.initialwert') }}</th>
|
</select>
|
||||||
<th class="cd-table-header">{{ $t('skill.improvable') }}</th>
|
</div>
|
||||||
<th class="cd-table-header">{{ $t('skill.innateskill') }}</th>
|
<div class="filter-item">
|
||||||
<th class="cd-table-header">{{ $t('skill.description') }}</th>
|
<label>{{ $t('skill.difficulty') }}:</label>
|
||||||
<th class="cd-table-header">{{ $t('skill.bonusskill') }}</th>
|
<select v-model="filterDifficulty">
|
||||||
<th class="cd-table-header">{{ $t('skill.quelle') }}</th>
|
<option value="">{{ $t('all') || 'All' }}</option>
|
||||||
<th class="cd-table-header">{{ $t('skill.system') }}</th>
|
<option v-for="diff in availableDifficulties" :key="diff" :value="diff">{{ diff }}</option>
|
||||||
<th class="cd-table-header"> </th>
|
</select>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
<div class="filter-item">
|
||||||
<tbody>
|
<label>{{ $t('skill.improvable') }}:</label>
|
||||||
<template v-for="(dtaItem, index) in filteredAndSortedSkills" :key="dtaItem.id">
|
<select v-model="filterImprovable">
|
||||||
<tr v-if="editingIndex !== index">
|
<option value="">{{ $t('all') || 'All' }}</option>
|
||||||
<td>{{ dtaItem.id || '' }}</td>
|
<option value="true">{{ $t('yes') || 'Yes' }}</option>
|
||||||
<td>{{ dtaItem.category|| '-' }}</td>
|
<option value="false">{{ $t('no') || 'No' }}</option>
|
||||||
<td>{{ dtaItem.name || '-' }}</td>
|
</select>
|
||||||
<td>{{ dtaItem.initialwert || '0' }}</td>
|
</div>
|
||||||
<td>{{ dtaItem.improvable || '0' }}</td>
|
<div class="filter-item">
|
||||||
<td>{{ dtaItem.innateskill || '0' }}</td>
|
<label>{{ $t('skill.innateskill') }}:</label>
|
||||||
<td>{{ dtaItem.beschreibung || '-' }}</td>
|
<select v-model="filterInnateskill">
|
||||||
<td>{{ dtaItem.bonuseigenschaft || '-' }}</td>
|
<option value="">{{ $t('all') || 'All' }}</option>
|
||||||
<td>{{ dtaItem.quelle || '-' }}</td>
|
<option value="true">{{ $t('yes') || 'Yes' }}</option>
|
||||||
<td>{{ dtaItem.system || 'midgard' }}</td>
|
<option value="false">{{ $t('no') || 'No' }}</option>
|
||||||
<td>
|
</select>
|
||||||
<button @click="startEdit(index)">Edit</button>
|
</div>
|
||||||
</td>
|
<div class="filter-item">
|
||||||
</tr>
|
<label>{{ $t('skill.bonusskill') }}:</label>
|
||||||
<tr v-else>
|
<select v-model="filterBonuseigenschaft">
|
||||||
<td><input v-model="editedItem.id" style="width:20px;"/></td>
|
<option value="">{{ $t('all') || 'All' }}</option>
|
||||||
<td><select v-model="editedItem.category" style="width:80px;">
|
<option v-for="bonus in availableBonuseigenschaften" :key="bonus" :value="bonus">{{ bonus }}</option>
|
||||||
<option v-for="category in mdata['skillcategories']"
|
</select>
|
||||||
:key="category"
|
</div>
|
||||||
:value="category">
|
<button @click="clearFilters" class="btn-clear-filters">{{ $t('clearFilters') || 'Clear Filters' }}</button>
|
||||||
{{ category }}
|
|
||||||
</option>
|
|
||||||
</select></td>
|
|
||||||
<td><input v-model="editedItem.name"/></td>
|
|
||||||
<td><input v-model.number="editedItem.initialwert" type="number" style="width:40px;"/></td>
|
|
||||||
<td><input type="checkbox" :checked="true" v-model="editedItem.improvable" style="width:50px;"/></td>
|
|
||||||
<td><input type="checkbox" :checked="true" v-model="editedItem.innateskill" style="width:50px;"/></td>
|
|
||||||
<td><input v-model="editedItem.beschreibung" /></td>
|
|
||||||
<td><input v-model="editedItem.bonuseigenschaft" style="width:80px;" ></td>
|
|
||||||
<td><input v-model="editedItem.quelle" style="width:80px;"/></td>
|
|
||||||
<td><input v-model="editedItem.system" style="width:80px;"/></td>
|
|
||||||
<td>
|
|
||||||
<button @click="saveEdit(index)">Save</button>
|
|
||||||
<button @click="cancelEdit">Cancel</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div> <!--- end cd-list-->
|
|
||||||
</div> <!--- end character -datasheet-->
|
<div class="tables-container">
|
||||||
|
<table class="cd-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="cd-table-header">{{ $t('skill.id') }}</th>
|
||||||
|
<th class="cd-table-header">
|
||||||
|
{{ $t('skill.category') }}
|
||||||
|
<button @click="sortBy('category')">{{ sortField === 'category' ? (sortAsc ? '↑' : '↓') : '-' }}</button>
|
||||||
|
</th>
|
||||||
|
<th class="cd-table-header">
|
||||||
|
{{ $t('skill.name') }}
|
||||||
|
<button @click="sortBy('name')">{{ sortField === 'name' ? (sortAsc ? '↑' : '↓') : '-' }}</button>
|
||||||
|
</th>
|
||||||
|
<th class="cd-table-header">{{ $t('skill.difficulty') }}</th>
|
||||||
|
<th class="cd-table-header">{{ $t('skill.initialwert') }}</th>
|
||||||
|
<th class="cd-table-header">{{ $t('skill.improvable') }}</th>
|
||||||
|
<th class="cd-table-header">{{ $t('skill.innateskill') }}</th>
|
||||||
|
<th class="cd-table-header">{{ $t('skill.description') }}</th>
|
||||||
|
<th class="cd-table-header">{{ $t('skill.bonusskill') }}</th>
|
||||||
|
<th class="cd-table-header">{{ $t('skill.quelle') }}</th>
|
||||||
|
<th class="cd-table-header">{{ $t('skill.system') }}</th>
|
||||||
|
<th class="cd-table-header"> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-for="(dtaItem, index) in filteredAndSortedSkills" :key="dtaItem.id">
|
||||||
|
<!-- Display Mode -->
|
||||||
|
<tr v-if="editingIndex !== index">
|
||||||
|
<td>{{ dtaItem.id || '' }}</td>
|
||||||
|
<td>{{ formatCategories(dtaItem.categories) }}</td>
|
||||||
|
<td>{{ dtaItem.name || '-' }}</td>
|
||||||
|
<td>{{ formatDifficulties(dtaItem.difficulties) }}</td>
|
||||||
|
<td>{{ dtaItem.initialwert || '0' }}</td>
|
||||||
|
<td><input type="checkbox" :checked="dtaItem.improvable" disabled /></td>
|
||||||
|
<td><input type="checkbox" :checked="dtaItem.innateskill" disabled /></td>
|
||||||
|
<td>{{ dtaItem.beschreibung || '-' }}</td>
|
||||||
|
<td>{{ dtaItem.bonuseigenschaft || '-' }}</td>
|
||||||
|
<td>{{ formatQuelle(dtaItem) }}</td>
|
||||||
|
<td>{{ dtaItem.game_system || 'midgard' }}</td>
|
||||||
|
<td>
|
||||||
|
<button @click="startEdit(index)">Edit</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Edit Mode -->
|
||||||
|
<tr v-else>
|
||||||
|
<td><input v-model="editedItem.id" style="width:20px;" disabled /></td>
|
||||||
|
<td colspan="11">
|
||||||
|
<!-- Expanded edit form -->
|
||||||
|
<div class="edit-form">
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('skill.name') }}:</label>
|
||||||
|
<input v-model="editedItem.name" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('skill.initialwert') }}:</label>
|
||||||
|
<input v-model.number="editedItem.initialwert" type="number" style="width:60px;" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('skill.bonusskill') }}:</label>
|
||||||
|
<select v-model="editedItem.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>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('skill.improvable') }}:</label>
|
||||||
|
<input type="checkbox" v-model="editedItem.improvable" />
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('skill.innateskill') }}:</label>
|
||||||
|
<input type="checkbox" v-model="editedItem.innateskill" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field full-width">
|
||||||
|
<label>{{ $t('skill.description') }}:</label>
|
||||||
|
<input v-model="editedItem.beschreibung" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('skill.quelle') }}:</label>
|
||||||
|
<select v-model="editedItem.sourceCode" style="width:100px;">
|
||||||
|
<option v-for="source in availableSources" :key="source.code" :value="source.code">
|
||||||
|
{{ source.code }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="edit-field">
|
||||||
|
<label>{{ $t('skill.page') || 'Page' }}:</label>
|
||||||
|
<input v-model.number="editedItem.page_number" type="number" style="width:60px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field full-width">
|
||||||
|
<label>{{ $t('skill.categories') || 'Categories' }}:</label>
|
||||||
|
<div class="category-checkboxes">
|
||||||
|
<div v-for="category in mdata.categories" :key="category.id" class="category-checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:value="category.id"
|
||||||
|
v-model="editedItem.selectedCategories"
|
||||||
|
@change="onCategoryToggle(category.id)"
|
||||||
|
/>
|
||||||
|
<label>{{ category.name }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-field full-width">
|
||||||
|
<label>{{ $t('skill.difficulties') || 'Difficulties' }}:</label>
|
||||||
|
<div class="difficulty-selects">
|
||||||
|
<div v-for="catId in editedItem.selectedCategories" :key="catId" class="difficulty-select">
|
||||||
|
<span>{{ getCategoryName(catId) }}:</span>
|
||||||
|
<select v-model="editedItem.categoryDifficulties[catId]" style="width:120px;">
|
||||||
|
<option v-for="diff in mdata.difficulties" :key="diff.id" :value="diff.id">
|
||||||
|
{{ diff.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edit-actions">
|
||||||
|
<button @click="saveEdit(index)" class="btn-save">Save</button>
|
||||||
|
<button @click="cancelEdit" class="btn-cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- <style scoped> -->
|
|
||||||
<style>
|
<style>
|
||||||
.header-section {
|
/* All styles moved to main.css as per project conventions */
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 0.3rem;
|
|
||||||
height: fit-content;
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
.search-box {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.search-box input {
|
|
||||||
padding: 0.2rem;
|
|
||||||
width: 200px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.tables-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-wrapper-left {
|
|
||||||
flex: 6;
|
|
||||||
min-width: 0; /* Prevent table from overflowing */
|
|
||||||
}
|
|
||||||
.table-wrapper-right {
|
|
||||||
flex: 4;
|
|
||||||
min-width: 0; /* Prevent table from overflowing */
|
|
||||||
}
|
|
||||||
|
|
||||||
.cd-table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.cd-table-header {
|
|
||||||
background-color: #1da766;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import API from '../../utils/api'
|
import API from '../../utils/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "SkillView",
|
name: "SkillView",
|
||||||
props: {
|
props: {
|
||||||
@@ -131,7 +227,9 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
default: () => ({
|
default: () => ({
|
||||||
skills: [],
|
skills: [],
|
||||||
skillcategories: []
|
categories: [],
|
||||||
|
difficulties: [],
|
||||||
|
sources: []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -141,82 +239,247 @@ export default {
|
|||||||
sortField: 'name',
|
sortField: 'name',
|
||||||
sortAsc: true,
|
sortAsc: true,
|
||||||
editingIndex: -1,
|
editingIndex: -1,
|
||||||
editedItem: null
|
editedItem: null,
|
||||||
|
filterCategory: '',
|
||||||
|
filterDifficulty: '',
|
||||||
|
filterImprovable: '',
|
||||||
|
filterInnateskill: '',
|
||||||
|
filterBonuseigenschaft: '',
|
||||||
|
enhancedSkills: [],
|
||||||
|
availableSources: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async created() {
|
||||||
|
await this.loadEnhancedSkills()
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
filteredAndSortedSkills() {
|
availableCategories() {
|
||||||
if (!this.mdata?.skills) return [];
|
const categories = new Set()
|
||||||
|
this.enhancedSkills.forEach(skill => {
|
||||||
return [...this.mdata.skills]
|
if (skill.categories) {
|
||||||
.filter(skill => {
|
skill.categories.forEach(cat => categories.add(cat.category_name))
|
||||||
const searchLower = this.searchTerm.toLowerCase();
|
}
|
||||||
return !this.searchTerm ||
|
})
|
||||||
skill.name?.toLowerCase().includes(searchLower) ||
|
return Array.from(categories).sort()
|
||||||
skill.category?.toLowerCase().includes(searchLower);
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
const aValue = (a[this.sortField] || '').toLowerCase();
|
|
||||||
const bValue = (b[this.sortField] || '').toLowerCase();
|
|
||||||
return this.sortAsc
|
|
||||||
? aValue.localeCompare(bValue)
|
|
||||||
: bValue.localeCompare(aValue);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
sortedSkills() {
|
availableDifficulties() {
|
||||||
return [...this.mdata.skills].sort((a, b) => {
|
const difficulties = new Set()
|
||||||
const aValue = (a[this.sortField] || '').toLowerCase();
|
this.enhancedSkills.forEach(skill => {
|
||||||
const bValue = (b[this.sortField] || '').toLowerCase();
|
if (skill.difficulties) {
|
||||||
return this.sortAsc
|
skill.difficulties.forEach(diff => difficulties.add(diff))
|
||||||
? aValue.localeCompare(bValue)
|
}
|
||||||
: bValue.localeCompare(aValue);
|
})
|
||||||
});
|
return Array.from(difficulties).sort()
|
||||||
|
},
|
||||||
|
availableBonuseigenschaften() {
|
||||||
|
const bonuses = new Set()
|
||||||
|
this.enhancedSkills.forEach(skill => {
|
||||||
|
if (skill.bonuseigenschaft) {
|
||||||
|
bonuses.add(skill.bonuseigenschaft)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return Array.from(bonuses).sort()
|
||||||
|
},
|
||||||
|
filteredAndSortedSkills() {
|
||||||
|
let filtered = [...this.enhancedSkills]
|
||||||
|
|
||||||
|
// Apply search filter
|
||||||
|
if (this.searchTerm) {
|
||||||
|
const searchLower = this.searchTerm.toLowerCase()
|
||||||
|
filtered = filtered.filter(skill =>
|
||||||
|
skill.name?.toLowerCase().includes(searchLower) ||
|
||||||
|
this.formatCategories(skill.categories).toLowerCase().includes(searchLower)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply category filter
|
||||||
|
if (this.filterCategory) {
|
||||||
|
filtered = filtered.filter(skill =>
|
||||||
|
skill.categories && skill.categories.some(cat => cat.category_name === this.filterCategory)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply difficulty filter
|
||||||
|
if (this.filterDifficulty) {
|
||||||
|
filtered = filtered.filter(skill =>
|
||||||
|
skill.difficulties && skill.difficulties.includes(this.filterDifficulty)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply improvable filter
|
||||||
|
if (this.filterImprovable !== '') {
|
||||||
|
const improvableValue = this.filterImprovable === 'true'
|
||||||
|
filtered = filtered.filter(skill => skill.improvable === improvableValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply innateskill filter
|
||||||
|
if (this.filterInnateskill !== '') {
|
||||||
|
const innateskillValue = this.filterInnateskill === 'true'
|
||||||
|
filtered = filtered.filter(skill => skill.innateskill === innateskillValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply bonuseigenschaft filter
|
||||||
|
if (this.filterBonuseigenschaft) {
|
||||||
|
filtered = filtered.filter(skill => skill.bonuseigenschaft === this.filterBonuseigenschaft)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
filtered.sort((a, b) => {
|
||||||
|
let aValue, bValue
|
||||||
|
|
||||||
|
if (this.sortField === 'category') {
|
||||||
|
aValue = this.formatCategories(a.categories).toLowerCase()
|
||||||
|
bValue = this.formatCategories(b.categories).toLowerCase()
|
||||||
|
} else {
|
||||||
|
aValue = (a[this.sortField] || '').toString().toLowerCase()
|
||||||
|
bValue = (b[this.sortField] || '').toString().toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.sortAsc ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
return filtered
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
startEdit(index) {
|
async loadEnhancedSkills() {
|
||||||
this.editingIndex = index;
|
try {
|
||||||
this.editedItem = { ...this.filteredAndSortedSkills[index] };
|
const response = await API.get('/api/maintenance/skills-enhanced')
|
||||||
|
this.enhancedSkills = response.data.skills || []
|
||||||
|
this.availableSources = response.data.sources || []
|
||||||
|
// Also update mdata for compatibility
|
||||||
|
if (response.data.categories) {
|
||||||
|
this.mdata.categories = response.data.categories
|
||||||
|
}
|
||||||
|
if (response.data.difficulties) {
|
||||||
|
this.mdata.difficulties = response.data.difficulties
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load enhanced skills:', error)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
saveEdit(index) {
|
formatCategories(categories) {
|
||||||
//this.$emit('update-skill', { index, skill: this.editedItem });
|
if (!categories || categories.length === 0) return '-'
|
||||||
this.handleSkillUpdate({ index, skill: this.editedItem });
|
return categories.map(cat => cat.category_name).join(', ')
|
||||||
this.editingIndex = -1;
|
},
|
||||||
this.editedItem = null;
|
formatDifficulties(difficulties) {
|
||||||
|
if (!difficulties || difficulties.length === 0) return '-'
|
||||||
|
return difficulties.join(', ')
|
||||||
|
},
|
||||||
|
formatQuelle(skill) {
|
||||||
|
if (skill.source_id && this.availableSources.length > 0) {
|
||||||
|
const source = this.availableSources.find(s => s.id === skill.source_id)
|
||||||
|
if (source) {
|
||||||
|
return skill.page_number ? `${source.code}:${skill.page_number}` : source.code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return skill.quelle || '-'
|
||||||
|
},
|
||||||
|
getCategoryName(categoryId) {
|
||||||
|
const category = this.mdata.categories.find(c => c.id === categoryId)
|
||||||
|
return category ? category.name : `ID:${categoryId}`
|
||||||
|
},
|
||||||
|
startEdit(index) {
|
||||||
|
const skill = this.filteredAndSortedSkills[index]
|
||||||
|
|
||||||
|
// Initialize edit object
|
||||||
|
this.editedItem = {
|
||||||
|
...skill,
|
||||||
|
selectedCategories: skill.categories ? skill.categories.map(cat => cat.category_id) : [],
|
||||||
|
categoryDifficulties: {},
|
||||||
|
sourceCode: this.getSourceCode(skill.source_id),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map category difficulties
|
||||||
|
if (skill.categories) {
|
||||||
|
skill.categories.forEach(cat => {
|
||||||
|
this.editedItem.categoryDifficulties[cat.category_id] = cat.difficulty_id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editingIndex = index
|
||||||
|
},
|
||||||
|
getSourceCode(sourceId) {
|
||||||
|
if (!sourceId || !this.availableSources.length) return ''
|
||||||
|
const source = this.availableSources.find(s => s.id === sourceId)
|
||||||
|
return source ? source.code : ''
|
||||||
|
},
|
||||||
|
onCategoryToggle(categoryId) {
|
||||||
|
// If category was removed, also remove its difficulty setting
|
||||||
|
if (!this.editedItem.selectedCategories.includes(categoryId)) {
|
||||||
|
delete this.editedItem.categoryDifficulties[categoryId]
|
||||||
|
} else {
|
||||||
|
// Set a default difficulty if not already set
|
||||||
|
if (!this.editedItem.categoryDifficulties[categoryId] && this.mdata.difficulties.length > 0) {
|
||||||
|
// Find "normal" difficulty or use first one
|
||||||
|
const normalDiff = this.mdata.difficulties.find(d => d.name.toLowerCase() === 'normal')
|
||||||
|
this.editedItem.categoryDifficulties[categoryId] = normalDiff ? normalDiff.id : this.mdata.difficulties[0].id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async saveEdit(index) {
|
||||||
|
try {
|
||||||
|
// Find source ID from code
|
||||||
|
const source = this.availableSources.find(s => s.code === this.editedItem.sourceCode)
|
||||||
|
|
||||||
|
// Build category_difficulties array
|
||||||
|
const categoryDifficulties = this.editedItem.selectedCategories.map(catId => ({
|
||||||
|
category_id: catId,
|
||||||
|
difficulty_id: this.editedItem.categoryDifficulties[catId]
|
||||||
|
}))
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
id: this.editedItem.id,
|
||||||
|
name: this.editedItem.name,
|
||||||
|
beschreibung: this.editedItem.beschreibung,
|
||||||
|
game_system: this.editedItem.game_system || 'midgard',
|
||||||
|
initialwert: this.editedItem.initialwert,
|
||||||
|
bonuseigenschaft: this.editedItem.bonuseigenschaft,
|
||||||
|
improvable: this.editedItem.improvable,
|
||||||
|
innateskill: this.editedItem.innateskill,
|
||||||
|
source_id: source ? source.id : null,
|
||||||
|
page_number: this.editedItem.page_number || 0,
|
||||||
|
category_difficulties: categoryDifficulties
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await API.put(
|
||||||
|
`/api/maintenance/skills-enhanced/${this.editedItem.id}`,
|
||||||
|
updateData
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update the skill in the list using splice for proper reactivity
|
||||||
|
const skillIndex = this.enhancedSkills.findIndex(s => s.id === this.editedItem.id)
|
||||||
|
if (skillIndex !== -1) {
|
||||||
|
this.enhancedSkills.splice(skillIndex, 1, response.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editingIndex = -1
|
||||||
|
this.editedItem = null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update skill:', error)
|
||||||
|
alert('Failed to update skill: ' + (error.response?.data?.error || error.message))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
cancelEdit() {
|
cancelEdit() {
|
||||||
this.editingIndex = -1;
|
this.editingIndex = -1
|
||||||
this.editedItem = null;
|
this.editedItem = null
|
||||||
},
|
},
|
||||||
sortBy(field) {
|
sortBy(field) {
|
||||||
if (this.sortField === field) {
|
if (this.sortField === field) {
|
||||||
this.sortAsc = !this.sortAsc;
|
this.sortAsc = !this.sortAsc
|
||||||
} else {
|
} else {
|
||||||
this.sortField = field;
|
this.sortField = field
|
||||||
this.sortAsc = true;
|
this.sortAsc = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async handleSkillUpdate({ index, skill }) {
|
clearFilters() {
|
||||||
try {
|
this.searchTerm = ''
|
||||||
const response = await API.put(
|
this.filterCategory = ''
|
||||||
`/api/maintenance/skills/${skill.id}`, skill,
|
this.filterDifficulty = ''
|
||||||
{
|
this.filterImprovable = ''
|
||||||
headers: {
|
this.filterInnateskill = ''
|
||||||
Authorization: `Bearer ${localStorage.getItem('token')}` ,
|
this.filterBonuseigenschaft = ''
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (!response.statusText== "OK") throw new Error('Update failed');
|
|
||||||
const updatedSkill = response.data;
|
|
||||||
// Update the skill in mdata
|
|
||||||
this.mdata.skills = this.mdata.skills.map(s =>
|
|
||||||
s.id === updatedSkill.id ? updatedSkill : s
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to update skill:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user