Falukant production, family and administration enhancements

This commit is contained in:
Torsten Schulz
2025-04-14 15:17:35 +02:00
parent 90b4f51dcb
commit b15d93a798
77 changed files with 2429 additions and 1093 deletions

View File

@@ -0,0 +1,116 @@
<template>
<div>
<h1>Edit Falukant User</h1>
<div>
<label>Username: <input type="text" v-model="user.username" /></label>
<label>Character name: <input type="text" v-model="user.characterName" /></label>
<button @click="searchUser">Search</button>
</div>
<ul v-for="user in users" class="user-list">
<li @click="selectUser(user)">{{ user.username }} ({{ user.falukantUser[0].character.definedFirstName.name }} {{
user.falukantUser[0].character.definedLastName.name }})</li>
</ul>
<div v-if="editableUser" class="edit-form">
<h2>User: {{ editableUser.username }}</h2>
<h3>Character-Name: {{ editableUser.falukantData[0].character.definedFirstName.name }} {{
editableUser.falukantData[0].character.definedLastName.name }}</h3>
<label>Money: <input type="number" v-model="editableUser.falukantData[0].money" /></label>
<label>Age: <input type="number" v-model="age" /></label>
<label>Noble title:
<select v-model="editableUser.falukantData[0].character.title_of_nobility">
<option v-for="title in titles" :value="title.id">{{ $t(`falukant.titles.male.${title.labelTr}`) }}</option>
</select>
</label>
<label>House: <select v-model="editableUser.falukantData[0].house">
<option v-for="house in houses" :value="house.id">{{ $t(`${house.labelTr}`) }}</option>
</select>
</label>
<button @click="saveUser">Save</button>
<button @click="deleteUser">Delete</button>
</div>
</div>
</template>
<script lang="js">
import { mapState } from 'vuex';
import { mapActions } from 'vuex';
import apiClient from '@/utils/axios.js';
export default {
name: 'AdminFalukantEditUserView',
data() {
return {
user: {
username: '',
characterName: ''
},
users: [],
editableUser: null,
age: null,
originalAge: null,
originalUser: null,
titles: [],
}
},
computed: {
...mapState('falukant', ['user'])
},
async mounted() {
const titlesResult = await apiClient.get('/api/falukant/nobility/titels');
this.titles = titlesResult.data;
},
methods: {
async searchUser() {
const userResult = await apiClient.post('/api/admin/falukant/searchuser', {
userName: this.user.username,
characterName: this.user.characterName
});
this.users = userResult.data;
},
async selectUser(user) {
const userResult = await apiClient.get(`/api/admin/falukant/getuser/${user.id}`);
this.editableUser = userResult.data;
this.originalUser = JSON.parse(JSON.stringify(this.editableUser));
this.age = Math.floor((Date.now() - new Date(this.editableUser.falukantData[0].character.birthdate)) / (24 * 60 * 60 * 1000));
this.originalAge = this.age;
this.users = [];
},
async saveUser() {
const dataToChange = {
id: this.editableUser.falukantData[0].id,
};
if (this.editableUser.falukantData[0].money != this.originalUser.falukantData[0].money) {
dataToChange.money = this.editableUser.falukantData[0].money;
}
if (this.editableUser.falukantData[0].character.title_of_nobility != this.originalUser.falukantData[0].character.title_of_nobility) {
dataToChange.title_of_nobility = this.editableUser.falukantData[0].character.title_of_nobility;
}
if (this.originalAge != this.age) {
dataToChange.age = this.age;
}
try {
await apiClient.post(`/api/admin/falukant/edituser`, dataToChange);
this.$root.$refs.messageDialog.open('tr:admin.falukant.edituser.success');
} catch (error) {
this.$root.$refs.errorDialog.open('tr:admin.falukant.edituser.error');
}
},
async deleteUser() {
const dataToChange = {
id: this.editableUser.falukantData[0].id,
};
}
}
}
</script>
<style lang="scss" scoped>
.user-list>li {
cursor: pointer;
color: #0066ff;
}
.edit-form label {
display: block;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,287 @@
<template>
<div class="contenthidden">
<StatusBar />
<div class="contentscroll">
<!-- Titel -->
<h2>{{ $t('falukant.family.title') }}</h2>
<!-- Ehepartner -->
<div class="spouse-section">
<h3>{{ $t('falukant.family.spouse.title') }}</h3>
<div v-if="relationships.length > 0">
<table>
<tr>
<td>{{ $t('falukant.family.relationships.name') }}</td>
<td>
{{ $t('falukant.titles.' + relationships[0].character2.gender + '.' +
relationships[0].character2.nobleTitle) }}
{{ relationships[0].character2.firstName }}
</td>
</tr>
<tr>
<td>{{ $t('falukant.family.spouse.age') }}</td>
<td>{{ relationships[0].character2.age }}</td>
</tr>
<tr>
<td>{{ $t('falukant.family.spouse.status') }}</td>
<td>{{ $t('falukant.family.statuses.' + relationships[0].relationshipType) }}</td>
</tr>
</table>
<div v-if="relationships[0].relationshipType === 'wooing'">
<h3>{{ $t('falukant.family.spouse.wooing.gifts') }}</h3>
<table class="spouse-table">
<thead>
<tr>
<th></th>
<th>{{ $t('falukant.family.spouse.wooing.gift') }}</th>
<th>{{ $t('falukant.family.spouse.wooing.value') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="gift in gifts" :key="gift.id">
<td><input type="radio" name="gift" :value="gift.id" v-model="selectedGiftId"></td>
<td>{{ $t(`falukant.gifts.${gift.name}`) }}</td>
<td>{{ formatCost(gift.cost) }}</td>
</tr>
</tbody>
</table>
<div>
<button @click="sendGift" class="button">{{ $t('falukant.family.spouse.wooing.sendGift') }}</button>
</div>
</div>
</div>
<div v-else-if="proposals && proposals.length > 0">
<table class="spouse-table">
<thead>
<tr>
<th>{{ $t('falukant.family.spouse.select') }}</th>
<th>{{ $t('falukant.family.spouse.name') }}</th>
<th>{{ $t('falukant.family.spouse.age') }}</th>
<th>{{ $t('falukant.family.spouse.marriagecost') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="proposal in proposals" :key="proposal.id">
<td><input type="radio" name="spouse" :value="proposal.proposedCharacterId"
v-model="selectedProposalId"></td>
<td>{{
$t(`falukant.titles.${proposal.proposedCharacterGender}.${proposal.proposedCharacterNobleTitle}`)
}} {{ proposal.proposedCharacterName }}</td>
<td>{{ proposal.proposedCharacterAge }}</td>
<td>{{ formatCost(proposal.cost) }}</td>
</tr>
</tbody>
</table>
<div>{{ $t('falukant.family.spouse.notice') }}</div>
<div v-if="selectedProposalId">
<button @click="acceptProposal">{{ $t('falukant.family.spouse.accept') }}</button>
</div>
</div>
</div>
<!-- Kinder -->
<div class="children-section">
<h3>{{ $t('falukant.family.children.title') }}</h3>
<div v-if="children && children.length > 0">
<table>
<thead>
<tr>
<th>{{ $t('falukant.family.children.name') }}</th>
<th>{{ $t('falukant.family.children.age') }}</th>
<th>{{ $t('falukant.family.children.actions') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(child, index) in children" :key="index">
<td>
{{ $t('falukant.titles.' + child.gender + '.' + child.title) }}
{{ child.name }}
</td>
<td>{{ child.age }}</td>
<td>
<button @click="showChildDetails(child)">
{{ $t('falukant.family.children.detailButton') }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else>
<p>{{ $t('falukant.family.children.none') }}</p>
</div>
</div>
<!-- Liebhaber / Geliebte -->
<div class="lovers-section">
<h3>{{ $t('falukant.family.lovers.title') }}</h3>
<div v-if="lovers && lovers.length > 0">
<ul>
<li v-for="(lover, idx) in lovers" :key="idx">
{{ $t('falukant.titles.' + lover.gender + '.' + lover.title) }} {{ lover.name }}
({{ $t('falukant.family.lovers.affection') }}: {{ lover.affection }})
</li>
</ul>
</div>
<div v-else>
<p>{{ $t('falukant.family.lovers.none') }}</p>
</div>
</div>
</div>
</div>
<!-- Dialog-Beispiele oder ähnliche Komponenten -->
<MessageDialog ref="messageDialog" />
<ErrorDialog ref="errorDialog" />
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue'
import MessageDialog from '@/dialogues/standard/MessageDialog.vue'
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue'
import apiClient from '@/utils/axios.js'
import { mapState } from 'vuex'
export default {
name: 'FamilyView',
components: {
StatusBar,
MessageDialog,
ErrorDialog
},
data() {
return {
relationships: [],
children: [],
lovers: [],
deathPartners: [],
proposals: [],
selectedProposalId: null,
gifts: [],
selectedGiftId: null
}
},
computed: {
...mapState(['socket', 'daemonSocket'])
},
async mounted() {
await this.loadFamilyData();
await this.loadGifts();
},
methods: {
async loadFamilyData() {
try {
const response = await apiClient.get('/api/falukant/family');
console.log(response.data);
this.relationships = response.data.relationships;
this.children = response.data.children;
this.lovers = response.data.lovers;
this.proposals = response.data.possiblePartners;
this.deathPartners = response.data.deathPartners;
} catch (error) {
console.error('Error loading family data:', error);
}
},
showChildDetails(child) {
console.log('Show details for child:', child);
},
formatCost(value) {
return new Intl.NumberFormat(navigator.language, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);
},
async acceptProposal() {
const response = await apiClient.post('/api/falukant/family/acceptmarriageproposal'
, { proposalId: this.selectedProposalId });
this.loadFamilyData();
},
async loadGifts() {
const response = await apiClient.get('/api/falukant/family/gifts');
this.gifts = response.data;
},
async sendGift() {
if (!this.selectedGiftId) {
alert('Please select a gift');
return;
}
const response = await apiClient.post('/api/falukant/family/gift'
, { giftId: this.selectedGiftId });
this.loadFamilyData();
}
}
}
</script>
<style scoped lang="scss">
.spouse-section,
.children-section,
.lovers-section {
border: 1px solid #ccc;
margin: 10px 0;
border-radius: 4px;
padding: 10px;
}
.spouse-section table,
.children-section table {
margin-top: 10px;
border-collapse: collapse;
}
.spouse-section th,
.spouse-section td,
.children-section th,
.children-section td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.spouse-section th {
background-color: #f2f2f2;
}
.children-section th {
background-color: #f9f9f9;
}
.lovers-section {
ul {
list-style: none;
padding-left: 0;
}
li {
padding: 4px 0;
}
}
.spouse-table th,
.spouse-table td {
white-space: nowrap;
}
.spouse-table th:first-child,
.spouse-table td:first-child {
width: 20px;
}
.spouse-table th:nth-child(3),
.spouse-table td:nth-child(3) {
width: 30px;
}
.spouse-table th:nth-child(4),
.spouse-table td:nth-child(4) {
width: 50px;
}
h2 {
padding-top: 20px;
}
</style>

View File

@@ -1,106 +1,118 @@
<template>
<div>
<h2>{{ $t('falukant.moneyHistory.title') }}</h2>
<div class="filter-section">
<label>{{ $t('falukant.moneyHistory.filter') }}</label>
<input v-model="filter" type="text" />
<button @click="fetchMoneyHistory(1)">{{ $t('falukant.moneyHistory.search') }}</button>
</div>
<table>
<thead>
<tr>
<th>{{ $t('falukant.moneyHistory.activity') }}</th>
<th>{{ $t('falukant.moneyHistory.moneyBefore') }}</th>
<th>{{ $t('falukant.moneyHistory.moneyAfter') }}</th>
<th>{{ $t('falukant.moneyHistory.changeValue') }}</th>
<th>{{ $t('falukant.moneyHistory.time') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(entry, index) in moneyHistory.data" :key="index">
<td>{{ $t(`falukant.moneyHistory.activities.${entry.activity}`) }}</td>
<td>{{ entry.moneyBefore }}</td>
<td>{{ entry.moneyAfter }}</td>
<td>{{ entry.changeValue }}</td>
<td>{{ new Date(entry.time).toLocaleString() }}</td>
</tr>
</tbody>
</table>
<div class="pagination">
<button
v-if="moneyHistory.currentPage > 1"
@click="fetchMoneyHistory(moneyHistory.currentPage - 1)"
>
{{ $t('falukant.moneyHistory.prev') }}
</button>
<span>
{{ moneyHistory.currentPage }} / {{ moneyHistory.totalPages }}
</span>
<button
v-if="moneyHistory.currentPage < moneyHistory.totalPages"
@click="fetchMoneyHistory(moneyHistory.currentPage + 1)"
>
{{ $t('falukant.moneyHistory.next') }}
</button>
</div>
<div class="moneyflow">
<StatusBar ref="statusBar" />
<h2>{{ $t('falukant.moneyHistory.title') }}</h2>
<div class="filter-section">
<label>{{ $t('falukant.moneyHistory.filter') }}</label>
<input v-model="filter" type="text" />
<button @click="fetchMoneyHistory(1)">{{ $t('falukant.moneyHistory.search') }}</button>
</div>
</template>
<script>
import apiClient from '@/utils/axios.js';
export default {
name: 'MoneyHistoryView',
data() {
return {
filter: '',
moneyHistory: {
data: [],
currentPage: 1,
totalPages: 1,
},
};
},
async mounted() {
await this.fetchMoneyHistory(1);
},
methods: {
async fetchMoneyHistory(page) {
try {
const response = await apiClient.post('/api/falukant/moneyhistory', {
page,
filter: this.filter,
});
this.moneyHistory = response.data;
} catch (error) {
console.error('Error fetching money history:', error);
}
<table>
<thead>
<tr>
<th>{{ $t('falukant.moneyHistory.activity') }}</th>
<th>{{ $t('falukant.moneyHistory.moneyBefore') }}</th>
<th>{{ $t('falukant.moneyHistory.moneyAfter') }}</th>
<th>{{ $t('falukant.moneyHistory.changeValue') }}</th>
<th>{{ $t('falukant.moneyHistory.time') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(entry, index) in moneyHistory.data" :key="index">
<td>{{ $t(`falukant.moneyHistory.activities.${entry.activity}`) }}</td>
<td>{{ entry.moneyBefore }}</td>
<td>{{ entry.moneyAfter }}</td>
<td>{{ entry.changeValue }}</td>
<td>{{ new Date(entry.time).toLocaleString() }}</td>
</tr>
</tbody>
</table>
<div class="pagination">
<button v-if="moneyHistory.currentPage > 1" @click="fetchMoneyHistory(moneyHistory.currentPage - 1)">
{{ $t('falukant.moneyHistory.prev') }}
</button>
<span>
{{ moneyHistory.currentPage }} / {{ moneyHistory.totalPages }}
</span>
<button v-if="moneyHistory.currentPage < moneyHistory.totalPages"
@click="fetchMoneyHistory(moneyHistory.currentPage + 1)">
{{ $t('falukant.moneyHistory.next') }}
</button>
</div>
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue'
import apiClient from '@/utils/axios.js';
export default {
name: 'MoneyHistoryView',
components: {
StatusBar,
},
data() {
return {
filter: '',
moneyHistory: {
data: [],
currentPage: 1,
totalPages: 1,
},
};
},
async mounted() {
await this.fetchMoneyHistory(1);
},
methods: {
async fetchMoneyHistory(page) {
try {
const response = await apiClient.post('/api/falukant/moneyhistory', {
page,
filter: this.filter,
});
this.moneyHistory = response.data;
} catch (error) {
console.error('Error fetching money history:', error);
}
},
};
</script>
<style scoped>
.filter-section {
margin-bottom: 1rem;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
}
th, td {
border: 1px solid #ccc;
padding: 8px;
text-align: left;
}
.pagination {
display: flex;
align-items: center;
gap: 1rem;
}
</style>
},
};
</script>
<style scoped>
.filter-section {
margin-bottom: 1rem;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
}
th,
td {
border: 1px solid #ccc;
padding: 8px;
text-align: left;
}
.pagination {
display: flex;
align-items: center;
gap: 1rem;
}
h2 {
padding-top: 20px;
}
.moneyflow {
overflow: auto;
height: 100%;
}
</style>

View File

@@ -11,9 +11,17 @@
<td>{{ falukantUser?.character.definedFirstName.name }} {{
falukantUser?.character.definedLastName.name }}</td>
</tr>
<tr>
<td>{{ $t('falukant.overview.metadata.nobleTitle') }}</td>
<td>{{ $t('falukant.titles.' + falukantUser?.character.gender + '.' + falukantUser?.character.nobleTitle.labelTr) }}</td>
</tr>
<tr>
<td>{{ $t('falukant.overview.metadata.money') }}</td>
<td>{{ falukantUser?.money }}</td>
<td>
{{ moneyValue != null
? moneyValue.toLocaleString(locale, { style: 'currency', currency: 'EUR' })
: '---' }}
</td>
</tr>
<tr>
<td>{{ $t('falukant.overview.metadata.age') }}</td>
@@ -150,7 +158,7 @@ export default {
const ageGroup = this.getAgeGroup(age);
const genderData = AVATAR_POSITIONS[gender] || {};
const position = genderData.positions?.[ageGroup] || { x: 0, y: 0 };
const width = genderData.width || 100;
const width = genderData.width || 100;
const height = genderData.height || 100;
return {
backgroundImage: `url(${imageUrl})`,
@@ -160,6 +168,13 @@ export default {
height: `${height}px`,
};
},
moneyValue() {
const m = this.falukantUser?.money;
return typeof m === 'string' ? parseFloat(m) : m;
},
locale() {
return window.navigator.language || 'en-US';
},
},
async mounted() {
await this.fetchFalukantUser();
@@ -171,7 +186,6 @@ export default {
}
if (this.daemonSocket) {
this.daemonSocket.addEventListener('message', (event) => {
console.log('incoming event', event);
try {
if (event.data === "ping") return;
const message = JSON.parse(event.data);
@@ -182,6 +196,8 @@ export default {
console.error('Error processing WebSocket message in FalukantOverviewView:', error);
}
});
} else {
console.log('no daemon socket');
}
},
beforeUnmount() {
@@ -281,4 +297,8 @@ export default {
background-size: cover;
image-rendering: crisp-edges;
}
h2 {
padding-top: 20px;
}
</style>