We can add weapons and equipment

This commit is contained in:
2025-12-28 14:48:59 +01:00
parent 4aea3559e6
commit 530931764d
7 changed files with 1294 additions and 22 deletions
+2
View File
@@ -4,6 +4,7 @@ import (
"bamort/character" "bamort/character"
"bamort/config" "bamort/config"
"bamort/database" "bamort/database"
"bamort/equipment"
"bamort/gsmaster" "bamort/gsmaster"
"bamort/importer" "bamort/importer"
"bamort/logger" "bamort/logger"
@@ -73,6 +74,7 @@ func main() {
// Register your module routes // Register your module routes
gsmaster.RegisterRoutes(protected) gsmaster.RegisterRoutes(protected)
character.RegisterRoutes(protected) character.RegisterRoutes(protected)
equipment.RegisterRoutes(protected)
maintenance.RegisterRoutes(protected) maintenance.RegisterRoutes(protected)
importer.RegisterRoutes(protected) importer.RegisterRoutes(protected)
pdfrender.RegisterRoutes(protected) pdfrender.RegisterRoutes(protected)
+63
View File
@@ -77,3 +77,66 @@ func DeleteAusruestung(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Ausruestung deleted successfully"}) c.JSON(http.StatusOK, gin.H{"message": "Ausruestung deleted successfully"})
} }
/*
Endpoints for Managing Weapons (Waffen)
*/
func CreateWaffe(c *gin.Context) {
var waffe models.EqWaffe
if err := c.ShouldBindJSON(&waffe); err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
if err := database.DB.Create(&waffe).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to create Waffe")
return
}
c.JSON(http.StatusCreated, waffe)
}
func ListWaffen(c *gin.Context) {
characterID := c.Param("character_id")
var waffen []models.EqWaffe
if err := database.DB.Where("character_id = ?", characterID).Find(&waffen).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve Waffen")
return
}
c.JSON(http.StatusOK, waffen)
}
func UpdateWaffe(c *gin.Context) {
waffeID := c.Param("waffe_id")
var waffe models.EqWaffe
if err := database.DB.First(&waffe, waffeID).Error; err != nil {
respondWithError(c, http.StatusNotFound, "Waffe not found")
return
}
if err := c.ShouldBindJSON(&waffe); err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
if err := database.DB.Save(&waffe).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to update Waffe")
return
}
c.JSON(http.StatusOK, waffe)
}
func DeleteWaffe(c *gin.Context) {
waffeID := c.Param("waffe_id")
if err := database.DB.Delete(&models.EqWaffe{}, waffeID).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to delete Waffe")
return
}
c.JSON(http.StatusOK, gin.H{"message": "Waffe deleted successfully"})
}
+21
View File
@@ -0,0 +1,21 @@
package equipment
import (
"github.com/gin-gonic/gin"
)
func RegisterRoutes(r *gin.RouterGroup) {
// Equipment (Ausrüstung) routes
equipGrp := r.Group("/equipment")
equipGrp.POST("", CreateAusruestung)
equipGrp.GET("/character/:character_id", ListAusruestung)
equipGrp.PUT("/:ausruestung_id", UpdateAusruestung)
equipGrp.DELETE("/:ausruestung_id", DeleteAusruestung)
// Weapon (Waffen) routes
weaponGrp := r.Group("/weapons")
weaponGrp.POST("", CreateWaffe)
weaponGrp.GET("/character/:character_id", ListWaffen)
weaponGrp.PUT("/:waffe_id", UpdateWaffe)
weaponGrp.DELETE("/:waffe_id", DeleteWaffe)
}
+469 -7
View File
@@ -1,5 +1,12 @@
<template> <template>
<div class="fullwidth-container"> <div class="fullwidth-container">
<div class="header-section">
<h2>{{ $t('EquipmentView') }}</h2>
<button @click="openAddEquipmentDialog" class="btn-add-equipment">
{{ $t('equipment.add') }}
</button>
</div>
<div class="cd-list"> <div class="cd-list">
<table class="cd-table"> <table class="cd-table">
<thead> <thead>
@@ -11,11 +18,12 @@
<th>{{ $t('equipment.amount') }}</th> <th>{{ $t('equipment.amount') }}</th>
<th>{{ $t('equipment.contained_in') }}</th> <th>{{ $t('equipment.contained_in') }}</th>
<th>{{ $t('equipment.bonus') }}</th> <th>{{ $t('equipment.bonus') }}</th>
<th>{{ $t('equipment.actions') }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template v-for="equipment in character.ausruestung"> <template v-if="character.ausruestung && character.ausruestung.length > 0">
<tr> <tr v-for="equipment in character.ausruestung" :key="equipment.id">
<td>{{ equipment.name || '-' }}</td> <td>{{ equipment.name || '-' }}</td>
<td>{{ equipment.beschreibung || '-' }}</td> <td>{{ equipment.beschreibung || '-' }}</td>
<td>{{ equipment.gewicht || '-' }}</td> <td>{{ equipment.gewicht || '-' }}</td>
@@ -23,20 +31,366 @@
<td>{{ equipment.anzahl || '-' }}</td> <td>{{ equipment.anzahl || '-' }}</td>
<td>{{ equipment.beinhaltet_in || '-' }}</td> <td>{{ equipment.beinhaltet_in || '-' }}</td>
<td>{{ equipment.bonus || '-' }}</td> <td>{{ equipment.bonus || '-' }}</td>
<td class="action-cell">
<button @click="deleteEquipment(equipment)" class="btn-delete" title="Löschen">
🗑
</button>
</td>
</tr>
</template>
<template v-else>
<tr>
<td colspan="8" class="empty-state">{{ $t('equipment.noEquipment') }}</td>
</tr> </tr>
</template> </template>
</tbody> </tbody>
</table> </table>
</div> <!--- end cd-list--> </div>
</div> <!--- end character -datasheet-->
<!-- Dialog für Ausrüstung hinzufügen -->
<div v-if="showAddDialog" class="modal-overlay" @click.self="closeDialog">
<div class="modal-content">
<div class="modal-header">
<h3>{{ $t('equipment.addEquipment') }}</h3>
<button @click="closeDialog" class="close-button">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>{{ $t('equipment.search') }}:</label>
<input
v-model="searchQuery"
type="text"
:placeholder="$t('equipment.searchPlaceholder')"
@input="filterEquipment"
/>
</div>
<div v-if="isLoading" class="loading">{{ $t('equipment.loading') }}</div>
<div v-else-if="filteredMasterEquipment.length > 0" class="equipment-list">
<div
v-for="equipment in filteredMasterEquipment"
:key="equipment.id"
class="equipment-item"
:class="{ selected: selectedEquipment && selectedEquipment.id === equipment.id }"
@click="selectEquipment(equipment)"
>
<div class="equipment-name">{{ equipment.name }}</div>
<div class="equipment-details">
{{ equipment.beschreibung || '-' }} |
{{ equipment.weight }}kg |
{{ equipment.value || '-' }}
</div>
</div>
</div>
<div v-else class="no-results">
{{ searchQuery ? $t('equipment.noResults') : $t('equipment.noMasterData') }}
</div>
<div v-if="selectedEquipment" class="selected-equipment-details">
<h4>{{ $t('equipment.selectedEquipment') }}</h4>
<p><strong>{{ $t('equipment.name') }}:</strong> {{ selectedEquipment.name }}</p>
<p><strong>{{ $t('equipment.description') }}:</strong> {{ selectedEquipment.beschreibung || '-' }}</p>
<p><strong>{{ $t('equipment.weight') }}:</strong> {{ selectedEquipment.weight }}kg</p>
<p><strong>{{ $t('equipment.value') }}:</strong> {{ selectedEquipment.value || '-' }}</p>
<div class="form-group">
<label>{{ $t('equipment.amount') }}:</label>
<input v-model.number="equipmentAmount" type="number" min="1" value="1" />
</div>
</div>
</div>
<div class="modal-footer">
<button @click="closeDialog" class="btn-cancel">{{ $t('equipment.cancel') }}</button>
<button
@click="addEquipment"
class="btn-confirm"
:disabled="!selectedEquipment || isSubmitting"
>
{{ isSubmitting ? $t('equipment.adding') : $t('equipment.add') }}
</button>
</div>
</div>
</div>
</div>
</template> </template>
<style> <style scoped>
.fullwidth-container {
padding: 1rem;
}
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.btn-add-equipment {
padding: 8px 16px;
background: #1da766;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
transition: background 0.2s ease;
}
.btn-add-equipment:hover {
background: #16a085;
}
.cd-table {
width: 100%;
border-collapse: collapse;
}
.cd-table th,
.cd-table td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.cd-table th {
background-color: #1da766;
color: white;
font-weight: bold;
}
.empty-state {
text-align: center;
color: #999;
font-style: italic;
padding: 2rem !important;
}
.action-cell {
text-align: center;
}
.btn-delete {
background: none;
border: none;
cursor: pointer;
font-size: 1.2rem;
padding: 4px 8px;
transition: transform 0.2s ease;
}
.btn-delete:hover {
transform: scale(1.2);
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
display: flex;
flex-direction: column;
animation: modalSlideIn 0.3s ease;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.modal-header {
padding: 20px;
border-bottom: 2px solid #1da766;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
color: #333;
}
.close-button {
background: none;
border: none;
font-size: 28px;
cursor: pointer;
color: #666;
line-height: 1;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.close-button:hover {
color: #333;
}
.modal-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.modal-footer {
padding: 15px 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
.form-group input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.loading {
text-align: center;
padding: 2rem;
color: #666;
}
.equipment-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 15px;
}
.equipment-item {
padding: 12px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background 0.2s ease;
}
.equipment-item:hover {
background: #f5f5f5;
}
.equipment-item.selected {
background: #e8f5e9;
border-left: 3px solid #1da766;
}
.equipment-name {
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.equipment-details {
font-size: 0.9em;
color: #666;
}
.no-results {
text-align: center;
padding: 2rem;
color: #999;
font-style: italic;
}
.selected-equipment-details {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin-top: 15px;
}
.selected-equipment-details h4 {
margin-top: 0;
color: #1da766;
}
.selected-equipment-details p {
margin: 8px 0;
}
.btn-confirm {
padding: 8px 20px;
background: #1da766;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: background 0.2s ease;
}
.btn-confirm:hover:not(:disabled) {
background: #16a085;
}
.btn-confirm:disabled {
background: #ccc;
cursor: not-allowed;
}
.btn-cancel {
padding: 8px 20px;
background: #6c757d;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s ease;
}
.btn-cancel:hover {
background: #5a6268;
}
</style> </style>
<script> <script>
import API from '@/utils/api'
export default { export default {
name: "EquipmentView", name: "EquipmentView",
props: { props: {
@@ -44,6 +398,114 @@ name: "EquipmentView",
type: Object, type: Object,
required: true required: true
} }
},
data() {
return {
showAddDialog: false,
isLoading: false,
isSubmitting: false,
masterEquipment: [],
filteredMasterEquipment: [],
selectedEquipment: null,
searchQuery: '',
equipmentAmount: 1
}
},
created() {
this.$api = API
},
methods: {
async openAddEquipmentDialog() {
this.showAddDialog = true
this.isLoading = true
try {
const response = await this.$api.get('/api/maintenance/equipment')
this.masterEquipment = response.data || []
this.filteredMasterEquipment = this.masterEquipment
} catch (error) {
console.error('Fehler beim Laden der Ausrüstungs-Stammdaten:', error)
alert(this.$t('equipment.loadError') + ': ' + (error.response?.data?.error || error.message))
} finally {
this.isLoading = false
}
},
filterEquipment() {
const query = this.searchQuery.toLowerCase().trim()
if (!query) {
this.filteredMasterEquipment = this.masterEquipment
} else {
this.filteredMasterEquipment = this.masterEquipment.filter(equipment =>
equipment.name.toLowerCase().includes(query) ||
(equipment.beschreibung && equipment.beschreibung.toLowerCase().includes(query))
)
}
},
selectEquipment(equipment) {
this.selectedEquipment = equipment
},
async addEquipment() {
if (!this.selectedEquipment) {
alert(this.$t('equipment.pleaseSelect'))
return
}
this.isSubmitting = true
try {
const equipmentData = {
character_id: this.character.id,
name: this.selectedEquipment.name,
beschreibung: this.selectedEquipment.beschreibung || '',
gewicht: this.selectedEquipment.weight || 0,
wert: this.selectedEquipment.value || 0,
anzahl: this.equipmentAmount,
bonus: this.selectedEquipment.bonus || 0,
beinhaltet_in: '',
contained_in: 0,
container_type: ''
}
await this.$api.post('/api/equipment', equipmentData)
alert(this.$t('equipment.addSuccess'))
this.closeDialog()
this.$emit('character-updated')
} catch (error) {
console.error('Fehler beim Hinzufügen der Ausrüstung:', error)
alert(this.$t('equipment.addError') + ': ' + (error.response?.data?.error || error.message))
} finally {
this.isSubmitting = false
}
},
async deleteEquipment(equipment) {
if (!confirm(this.$t('equipment.confirmDelete').replace('{name}', equipment.name))) {
return
}
try {
await this.$api.delete(`/api/equipment/${equipment.id}`)
alert(this.$t('equipment.deleteSuccess'))
this.$emit('character-updated')
} catch (error) {
console.error('Fehler beim Löschen der Ausrüstung:', error)
alert(this.$t('equipment.deleteError') + ': ' + (error.response?.data?.error || error.message))
}
},
closeDialog() {
this.showAddDialog = false
this.selectedEquipment = null
this.searchQuery = ''
this.equipmentAmount = 1
this.filteredMasterEquipment = []
this.masterEquipment = []
}
}
} }
};
</script> </script>
+645 -13
View File
@@ -1,5 +1,12 @@
<template> <template>
<div class="cd-view"> <div class="cd-view">
<div class="header-section">
<h2>{{ $t('WeaponView') }}</h2>
<button @click="openAddWeaponDialog" class="btn-add-weapon">
{{ $t('weapon.add') }}
</button>
</div>
<div class="cd-list"> <div class="cd-list">
<table class="cd-table"> <table class="cd-table">
<thead> <thead>
@@ -11,33 +18,493 @@
<th>{{ $t('weapon.amount') }}</th> <th>{{ $t('weapon.amount') }}</th>
<th>{{ $t('weapon.contained_in') }}</th> <th>{{ $t('weapon.contained_in') }}</th>
<th>{{ $t('weapon.bonus') }}</th> <th>{{ $t('weapon.bonus') }}</th>
<th>{{ $t('weapon.actions') }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template v-for="weapon in character.waffen"> <template v-if="character.waffen && character.waffen.length > 0">
<tr v-for="weapon in character.waffen" :key="weapon.id">
<td>{{ weapon.name || '-' }}<span v-if="weapon.ist_magisch" class="magic-indicator">*</span></td>
<td>{{ weapon.beschreibung || '-' }}</td>
<td>{{ weapon.gewicht || '-' }}</td>
<td>{{ weapon.wert || '-' }}</td>
<td>{{ weapon.anzahl || '-' }}</td>
<td>{{ weapon.beinhaltet_in || '-' }}</td>
<td>{{ weapon.anb || '-' }}/{{ weapon.abwb || '-' }}</td>
<td class="action-cell">
<button @click="editWeapon(weapon)" class="btn-edit" title="Bearbeiten">
</button>
<button @click="deleteWeapon(weapon)" class="btn-delete" title="Löschen">
🗑
</button>
</td>
</tr>
</template>
<template v-else>
<tr> <tr>
<td>{{ weapon.name || '-' }}</td> <td colspan="8" class="empty-state">{{ $t('weapon.noWeapons') }}</td>
<td>{{ weapon.description || '-' }}</td>
<td>{{ weapon.weight || '-' }}</td>
<td>{{ weapon.value || '-' }}</td>
<td>{{ weapon.amount || '-' }}</td>
<td>{{ weapon.contained_in || '-' }}</td>
<td>{{ weapon.Anb || '-' }}/{{ weapon.Abwb || '-' }}</td>
</tr> </tr>
</template> </template>
</tbody> </tbody>
</table> </table>
</div> <!--- end cd-list--> </div>
</div> <!--- end character -datasheet-->
<!-- Dialog für Waffe hinzufügen -->
<div v-if="showAddDialog" class="modal-overlay" @click.self="closeDialog">
<div class="modal-content">
<div class="modal-header">
<h3>{{ $t('weapon.addWeapon') }}</h3>
<button @click="closeDialog" class="close-button">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>{{ $t('weapon.search') }}:</label>
<input
v-model="searchQuery"
type="text"
:placeholder="$t('weapon.searchPlaceholder')"
@input="filterWeapons"
/>
</div>
<div v-if="isLoading" class="loading">{{ $t('weapon.loading') }}</div>
<div v-else-if="filteredMasterWeapons.length > 0" class="weapon-list">
<div
v-for="weapon in filteredMasterWeapons"
:key="weapon.id"
class="weapon-item"
:class="{ selected: selectedWeapon && selectedWeapon.id === weapon.id }"
@click="selectWeapon(weapon)"
>
<div class="weapon-name">{{ weapon.name }}</div>
<div class="weapon-details">
{{ weapon.damage || '-' }} |
{{ weapon.weight }}kg |
{{ weapon.value || '-' }}
</div>
</div>
</div>
<div v-else class="no-results">
{{ searchQuery ? $t('weapon.noResults') : $t('weapon.noMasterData') }}
</div>
<div v-if="selectedWeapon" class="selected-weapon-details">
<h4>{{ $t('weapon.selectedWeapon') }}</h4>
<p><strong>{{ $t('weapon.name') }}:</strong> {{ selectedWeapon.name }}</p>
<p><strong>{{ $t('weapon.damage') }}:</strong> {{ selectedWeapon.damage || '-' }}</p>
<p><strong>{{ $t('weapon.weight') }}:</strong> {{ selectedWeapon.weight }}kg</p>
<p><strong>{{ $t('weapon.value') }}:</strong> {{ selectedWeapon.value || '-' }}</p>
<div class="form-group">
<label>{{ $t('weapon.amount') }}:</label>
<input v-model.number="weaponAmount" type="number" min="1" value="1" />
</div>
</div>
</div>
<div class="modal-footer">
<button @click="closeDialog" class="btn-cancel">{{ $t('weapon.cancel') }}</button>
<button
@click="addWeapon"
class="btn-confirm"
:disabled="!selectedWeapon || isSubmitting"
>
{{ isSubmitting ? $t('weapon.adding') : $t('weapon.add') }}
</button>
</div>
</div>
</div>
<!-- Dialog für Waffe bearbeiten -->
<div v-if="showEditDialog" class="modal-overlay" @click.self="closeEditDialog">
<div class="modal-content">
<div class="modal-header">
<h3>{{ $t('weapon.editWeapon') }}</h3>
<button @click="closeEditDialog" class="close-button">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>{{ $t('weapon.name') }}:</label>
<input v-model="editingWeapon.name" type="text" disabled />
</div>
<div class="form-group">
<label>{{ $t('weapon.description') }}:</label>
<textarea v-model="editingWeapon.beschreibung" rows="3"></textarea>
</div>
<div class="form-group">
<label>
<input type="checkbox" v-model="editingWeapon.ist_magisch" />
{{ $t('weapon.magical') }}
</label>
</div>
<div class="form-row">
<div class="form-group">
<label>{{ $t('weapon.amount') }}:</label>
<input v-model.number="editingWeapon.anzahl" type="number" min="1" />
</div>
<div class="form-group">
<label>{{ $t('weapon.value') }}:</label>
<input v-model.number="editingWeapon.wert" type="number" min="0" step="0.01" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>{{ $t('weapon.attackBonus') }} (Anb):</label>
<input v-model.number="editingWeapon.anb" type="number" />
</div>
<div class="form-group">
<label>{{ $t('weapon.defenseBonus') }} (Abwb):</label>
<input v-model.number="editingWeapon.abwb" type="number" />
</div>
</div>
<div class="form-group">
<label>{{ $t('weapon.damageBonus') }} (Schb):</label>
<input v-model.number="editingWeapon.schb" type="number" />
</div>
</div>
<div class="modal-footer">
<button @click="closeEditDialog" class="btn-cancel">{{ $t('weapon.cancel') }}</button>
<button
@click="saveWeapon"
class="btn-confirm"
:disabled="isSubmitting"
>
{{ isSubmitting ? $t('weapon.saving') : $t('weapon.save') }}
</button>
</div>
</div>
</div>
</div>
</template> </template>
<style> <style scoped>
.cd-view {
padding: 1rem;
}
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.btn-add-weapon {
padding: 8px 16px;
background: #1da766;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
transition: background 0.2s ease;
}
.btn-add-weapon:hover {
background: #16a085;
}
.cd-table {
width: 100%;
border-collapse: collapse;
}
.cd-table th,
.cd-table td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.cd-table th {
background-color: #1da766;
color: white;
font-weight: bold;
}
.empty-state {
text-align: center;
color: #999;
font-style: italic;
padding: 2rem !important;
}
.magic-indicator {
color: #9c27b0;
font-weight: bold;
margin-left: 4px;
}
.action-cell {
text-align: center;
}
.btn-edit {
background: none;
border: none;
cursor: pointer;
font-size: 1.2rem;
padding: 4px 8px;
margin-right: 8px;
transition: transform 0.2s ease;
}
.btn-edit:hover {
transform: scale(1.2);
}
.btn-delete {
background: none;
border: none;
cursor: pointer;
font-size: 1.2rem;
padding: 4px 8px;
transition: transform 0.2s ease;
}
.btn-delete:hover {
transform: scale(1.2);
}
.form-row {
display: flex;
gap: 15px;
margin-bottom: 15px;
}
.form-row .form-group {
flex: 1;
margin-bottom: 0;
}
.form-group textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
resize: vertical;
font-family: inherit;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
display: flex;
flex-direction: column;
animation: modalSlideIn 0.3s ease;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.modal-header {
padding: 20px;
border-bottom: 2px solid #1da766;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
color: #333;
}
.close-button {
background: none;
border: none;
font-size: 28px;
cursor: pointer;
color: #666;
line-height: 1;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.close-button:hover {
color: #333;
}
.modal-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.modal-footer {
padding: 15px 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
.form-group input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.loading {
text-align: center;
padding: 2rem;
color: #666;
}
.weapon-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 15px;
}
.weapon-item {
padding: 12px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background 0.2s ease;
}
.weapon-item:hover {
background: #f5f5f5;
}
.weapon-item.selected {
background: #e8f5e9;
border-left: 3px solid #1da766;
}
.weapon-name {
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.weapon-details {
font-size: 0.9em;
color: #666;
}
.no-results {
text-align: center;
padding: 2rem;
color: #999;
font-style: italic;
}
.selected-weapon-details {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin-top: 15px;
}
.selected-weapon-details h4 {
margin-top: 0;
color: #1da766;
}
.selected-weapon-details p {
margin: 8px 0;
}
.btn-confirm {
padding: 8px 20px;
background: #1da766;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: background 0.2s ease;
}
.btn-confirm:hover:not(:disabled) {
background: #16a085;
}
.btn-confirm:disabled {
background: #ccc;
cursor: not-allowed;
}
.btn-cancel {
padding: 8px 20px;
background: #6c757d;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s ease;
}
.btn-cancel:hover {
background: #5a6268;
}
</style> </style>
<script> <script>
import API from '@/utils/api'
export default { export default {
name: "WeaponView", name: "WeaponView",
props: { props: {
@@ -45,6 +512,171 @@ name: "WeaponView",
type: Object, type: Object,
required: true required: true
} }
},
data() {
return {
showAddDialog: false,
showEditDialog: false,
isLoading: false,
isSubmitting: false,
masterWeapons: [],
filteredMasterWeapons: [],
selectedWeapon: null,
editingWeapon: null,
searchQuery: '',
weaponAmount: 1
}
},
created() {
this.$api = API
},
methods: {
async openAddWeaponDialog() {
this.showAddDialog = true
this.isLoading = true
try {
const response = await this.$api.get('/api/maintenance/weapons')
this.masterWeapons = response.data || []
this.filteredMasterWeapons = this.masterWeapons
} catch (error) {
console.error('Fehler beim Laden der Waffen-Stammdaten:', error)
alert(this.$t('weapon.loadError') + ': ' + (error.response?.data?.error || error.message))
} finally {
this.isLoading = false
}
},
filterWeapons() {
const query = this.searchQuery.toLowerCase().trim()
if (!query) {
this.filteredMasterWeapons = this.masterWeapons
} else {
this.filteredMasterWeapons = this.masterWeapons.filter(weapon =>
weapon.name.toLowerCase().includes(query) ||
(weapon.beschreibung && weapon.beschreibung.toLowerCase().includes(query))
)
}
},
selectWeapon(weapon) {
this.selectedWeapon = weapon
},
async addWeapon() {
if (!this.selectedWeapon) {
alert(this.$t('weapon.pleaseSelect'))
return
}
this.isSubmitting = true
try {
const weaponData = {
character_id: this.character.id,
name: this.selectedWeapon.name,
beschreibung: this.selectedWeapon.beschreibung || '',
gewicht: this.selectedWeapon.weight || 0,
wert: this.selectedWeapon.value || 0,
anzahl: this.weaponAmount,
ist_magisch: false,
anb: this.selectedWeapon.attack_bonus || 0,
abwb: this.selectedWeapon.defense_bonus || 0,
schb: this.selectedWeapon.damage_bonus || 0,
beinhaltet_in: '',
contained_in: 0,
container_type: ''
}
await this.$api.post('/api/weapons', weaponData)
alert(this.$t('weapon.addSuccess'))
this.closeDialog()
this.$emit('character-updated')
} catch (error) {
console.error('Fehler beim Hinzufügen der Waffe:', error)
alert(this.$t('weapon.addError') + ': ' + (error.response?.data?.error || error.message))
} finally {
this.isSubmitting = false
}
},
editWeapon(weapon) {
this.editingWeapon = {
id: weapon.id,
name: weapon.name,
beschreibung: weapon.beschreibung || '',
ist_magisch: weapon.ist_magisch || false,
anzahl: weapon.anzahl || 1,
wert: weapon.wert || 0,
anb: weapon.anb || 0,
abwb: weapon.abwb || 0,
schb: weapon.schb || 0
}
this.showEditDialog = true
},
async saveWeapon() {
if (!this.editingWeapon) {
return
}
this.isSubmitting = true
try {
const weaponData = {
beschreibung: this.editingWeapon.beschreibung,
ist_magisch: this.editingWeapon.ist_magisch,
anzahl: this.editingWeapon.anzahl,
wert: this.editingWeapon.wert,
anb: this.editingWeapon.anb,
abwb: this.editingWeapon.abwb,
schb: this.editingWeapon.schb
}
await this.$api.put(`/api/weapons/${this.editingWeapon.id}`, weaponData)
alert(this.$t('weapon.editSuccess'))
this.closeEditDialog()
this.$emit('character-updated')
} catch (error) {
console.error('Fehler beim Speichern der Waffe:', error)
alert(this.$t('weapon.editError') + ': ' + (error.response?.data?.error || error.message))
} finally {
this.isSubmitting = false
}
},
async deleteWeapon(weapon) {
if (!confirm(this.$t('weapon.confirmDelete').replace('{name}', weapon.name))) {
return
}
try {
await this.$api.delete(`/api/weapons/${weapon.id}`)
alert(this.$t('weapon.deleteSuccess'))
this.$emit('character-updated')
} catch (error) {
console.error('Fehler beim Löschen der Waffe:', error)
alert(this.$t('weapon.deleteError') + ': ' + (error.response?.data?.error || error.message))
}
},
closeEditDialog() {
this.showEditDialog = false
this.editingWeapon = null
},
closeDialog() {
this.showAddDialog = false
this.selectedWeapon = null
this.searchQuery = ''
this.weaponAmount = 1
this.filteredMasterWeapons = []
this.masterWeapons = []
}
}
} }
};
</script> </script>
+47
View File
@@ -54,6 +54,25 @@ export default {
quelle:'Quelle', quelle:'Quelle',
system:'System', system:'System',
personal_item:'Pers', personal_item:'Pers',
add: 'Ausrüstung hinzufügen',
addEquipment: 'Ausrüstung hinzufügen',
search: 'Suche',
searchPlaceholder: 'Ausrüstung suchen...',
loading: 'Lade Ausrüstung...',
noEquipment: 'Keine Ausrüstung vorhanden. Klicken Sie auf "Ausrüstung hinzufügen", um eine hinzuzufügen.',
noResults: 'Keine Ausrüstung gefunden.',
noMasterData: 'Keine Ausrüstungs-Stammdaten verfügbar.',
selectedEquipment: 'Ausgewählte Ausrüstung',
cancel: 'Abbrechen',
adding: 'Wird hinzugefügt...',
pleaseSelect: 'Bitte wählen Sie eine Ausrüstung aus.',
addSuccess: 'Ausrüstung erfolgreich hinzugefügt!',
addError: 'Fehler beim Hinzufügen der Ausrüstung',
loadError: 'Fehler beim Laden der Ausrüstungs-Stammdaten',
confirmDelete: 'Möchten Sie die Ausrüstung "{name}" wirklich löschen?',
deleteSuccess: 'Ausrüstung erfolgreich gelöscht!',
deleteError: 'Fehler beim Löschen der Ausrüstung',
actions: 'Aktionen'
}, },
skill:{ skill:{
id:'ID', id:'ID',
@@ -132,6 +151,34 @@ export default {
personal_item:'Pers', personal_item:'Pers',
quelle:'Quelle', quelle:'Quelle',
system:'System', system:'System',
add: 'Waffe hinzufügen',
addWeapon: 'Waffe hinzufügen',
search: 'Suche',
searchPlaceholder: 'Waffe suchen...',
loading: 'Lade Waffen...',
noWeapons: 'Keine Waffen vorhanden. Klicken Sie auf "Waffe hinzufügen", um eine hinzuzufügen.',
noResults: 'Keine Waffen gefunden.',
noMasterData: 'Keine Waffen-Stammdaten verfügbar.',
selectedWeapon: 'Ausgewählte Waffe',
cancel: 'Abbrechen',
adding: 'Wird hinzugefügt...',
pleaseSelect: 'Bitte wählen Sie eine Waffe aus.',
addSuccess: 'Waffe erfolgreich hinzugefügt!',
addError: 'Fehler beim Hinzufügen der Waffe',
loadError: 'Fehler beim Laden der Waffen-Stammdaten',
confirmDelete: 'Möchten Sie die Waffe "{name}" wirklich löschen?',
deleteSuccess: 'Waffe erfolgreich gelöscht!',
deleteError: 'Fehler beim Löschen der Waffe',
actions: 'Aktionen',
editWeapon: 'Waffe bearbeiten',
magical: 'Magisch',
attackBonus: 'Angriffsbonus',
defenseBonus: 'Abwehrbonus',
damageBonus: 'Schadensbonus',
save: 'Speichern',
saving: 'Wird gespeichert...',
editSuccess: 'Waffe erfolgreich aktualisiert!',
editError: 'Fehler beim Speichern der Waffe'
}, },
Weapons:'Waffen', Weapons:'Waffen',
weaponskill:{ weaponskill:{
+47 -2
View File
@@ -53,8 +53,25 @@ export default {
wert:'Value', wert:'Value',
quelle:'Source', quelle:'Source',
system:'System', system:'System',
personal_item:'Pers', personal_item:'Pers', add: 'Add Equipment',
}, addEquipment: 'Add Equipment',
search: 'Search',
searchPlaceholder: 'Search equipment...',
loading: 'Loading equipment...',
noEquipment: 'No equipment available. Click "Add Equipment" to add one.',
noResults: 'No equipment found.',
noMasterData: 'No equipment master data available.',
selectedEquipment: 'Selected Equipment',
cancel: 'Cancel',
adding: 'Adding...',
pleaseSelect: 'Please select equipment.',
addSuccess: 'Equipment successfully added!',
addError: 'Error adding equipment',
loadError: 'Error loading equipment master data',
confirmDelete: 'Do you really want to delete the equipment "{name}"?',
deleteSuccess: 'Equipment successfully deleted!',
deleteError: 'Error deleting equipment',
actions: 'Actions' },
skill:{ skill:{
id:'ID', id:'ID',
name:'Name', name:'Name',
@@ -132,6 +149,34 @@ export default {
personal_item:'Pers', personal_item:'Pers',
quelle:'Source', quelle:'Source',
system:'System', system:'System',
add: 'Add Weapon',
addWeapon: 'Add Weapon',
search: 'Search',
searchPlaceholder: 'Search weapon...',
loading: 'Loading weapons...',
noWeapons: 'No weapons available. Click "Add Weapon" to add one.',
noResults: 'No weapons found.',
noMasterData: 'No weapon master data available.',
selectedWeapon: 'Selected Weapon',
cancel: 'Cancel',
adding: 'Adding...',
pleaseSelect: 'Please select a weapon.',
addSuccess: 'Weapon successfully added!',
addError: 'Error adding weapon',
loadError: 'Error loading weapon master data',
confirmDelete: 'Do you really want to delete the weapon "{name}"?',
deleteSuccess: 'Weapon successfully deleted!',
deleteError: 'Error deleting weapon',
actions: 'Actions',
editWeapon: 'Edit Weapon',
magical: 'Magical',
attackBonus: 'Attack Bonus',
defenseBonus: 'Defense Bonus',
damageBonus: 'Damage Bonus',
save: 'Save',
saving: 'Saving...',
editSuccess: 'Weapon successfully updated!',
editError: 'Error saving weapon'
}, },
Weapons:'Waffen', Weapons:'Waffen',
weaponskill:{ weaponskill:{