- Swapped the order of parameters in the message dialog open method to prioritize message content over the title, enhancing user experience during reputation action notifications.
411 lines
17 KiB
Vue
411 lines
17 KiB
Vue
<template>
|
|
<div class="reputation-view">
|
|
<StatusBar />
|
|
<h2>{{ $t('falukant.reputation.title') }}</h2>
|
|
|
|
<div class="simple-tabs">
|
|
<button v-for="tab in tabs" :key="tab.value" :class="['simple-tab', { active: activeTab === tab.value }]"
|
|
@click="activeTab = tab.value">
|
|
{{ $t(tab.label) }}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="tab-content">
|
|
<div v-if="activeTab === 'overview'">
|
|
<p>
|
|
{{ $t('falukant.reputation.overview.current') }}:
|
|
<strong>{{ reputationDisplay }}</strong>
|
|
</p>
|
|
</div>
|
|
|
|
<div v-else-if="activeTab === 'party'">
|
|
<button @click.prevent="toggleNewPartyView()" type="button">
|
|
{{ $t('falukant.reputation.party.newpartyview.' + (newPartyView ? 'close' : 'open')) }}
|
|
</button>
|
|
|
|
<div v-if="newPartyView" class="new-party-form">
|
|
<label>
|
|
{{ $t('falukant.reputation.party.newpartyview.type') }}:
|
|
<select v-model.number="newPartyTypeId">
|
|
<option v-for="type in partyTypes" :key="type.id" :value="type.id">
|
|
{{ $t('falukant.party.type.' + type.tr) }}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
|
|
<div v-if="newPartyTypeId" class="party-options">
|
|
<label>
|
|
{{ $t('falukant.reputation.party.music.label') }}:
|
|
<select v-model.number="musicId">
|
|
<option v-for="m in musicTypes" :key="m.id" :value="m.id">
|
|
{{ $t(`falukant.reputation.party.music.${m.tr}`) }}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
|
|
<label>
|
|
{{ $t('falukant.reputation.party.banquette.label') }}:
|
|
<select v-model.number="banquetteId">
|
|
<option v-for="b in banquetteTypes" :key="b.id" :value="b.id">
|
|
{{ $t(`falukant.reputation.party.banquette.${b.tr}`) }}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
|
|
<label>
|
|
{{ $t('falukant.reputation.party.servants.label') }}:
|
|
<input type="number" v-model.number="servantRatio" min="1" max="50" />
|
|
{{ $t('falukant.reputation.party.servants.perPersons') }}
|
|
</label>
|
|
|
|
<label>
|
|
{{ $t('falukant.reputation.party.esteemedInvites.label') }}:
|
|
<multiselect v-model="selectedNobilityIds" :options="nobilityTitles" :multiple="true"
|
|
track-by="id" label="labelTr" :close-on-select="false" :preserve-search="true"
|
|
placeholder="">
|
|
<template #option="{ option }">
|
|
{{ $t('falukant.titles.male.' + option.labelTr) }}
|
|
</template>
|
|
<template #tag="{ option, remove }">
|
|
<span class="multiselect__tag">
|
|
{{ $t('falukant.titles.male.' + option.labelTr) }}
|
|
<i @click="remove(option.id)" class="multiselect__tag-icon"></i>
|
|
</span>
|
|
</template>
|
|
</multiselect>
|
|
</label>
|
|
|
|
<p class="total-cost">
|
|
{{ $t('falukant.reputation.party.totalCost') }}:
|
|
{{ formattedCost }}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<button type="button" @click="orderParty()">
|
|
{{ $t('falukant.reputation.party.order') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- In-Progress Parties -->
|
|
<div class="separator-class">
|
|
<h3>{{ $t('falukant.reputation.party.inProgress') }}</h3>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>{{ $t('falukant.reputation.party.type') }}</th>
|
|
<th>{{ $t('falukant.reputation.party.music.label') }}</th>
|
|
<th>{{ $t('falukant.reputation.party.banquette.label') }}</th>
|
|
<th>{{ $t('falukant.reputation.party.servants.label') }}</th>
|
|
<th>{{ $t('falukant.reputation.party.cost') }}</th>
|
|
<th>{{ $t('falukant.reputation.party.date') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="party in inProgressParties" :key="party.id">
|
|
<td>{{ $t('falukant.party.type.' + party.partyType.tr) }}</td>
|
|
<td>{{ $t('falukant.reputation.party.music.' + party.musicType.tr) }}</td>
|
|
<td>{{ $t('falukant.reputation.party.banquette.' + party.banquetteType.tr) }}</td>
|
|
<td>{{ party.servantRatio }}</td>
|
|
<td>{{ party.cost.toLocaleString($i18n.locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</td>
|
|
<td>{{ getPartyDate(party.createdAt).toLocaleString() }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Completed Parties -->
|
|
<div class="separator-class">
|
|
<h3>{{ $t('falukant.reputation.party.completed') }}</h3>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>{{ $t('falukant.reputation.party.type') }}</th>
|
|
<th>{{ $t('falukant.reputation.party.music.label') }}</th>
|
|
<th>{{ $t('falukant.reputation.party.banquette.label') }}</th>
|
|
<th>{{ $t('falukant.reputation.party.servants.label') }}</th>
|
|
<th>{{ $t('falukant.reputation.party.cost') }}</th>
|
|
<th>{{ $t('falukant.reputation.party.date') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="party in completedParties" :key="party.id">
|
|
<td>{{ $t('falukant.party.type.' + party.partyType.tr) }}</td>
|
|
<td>{{ $t('falukant.reputation.party.music.' + party.musicType.tr) }}</td>
|
|
<td>{{ $t('falukant.reputation.party.banquette.' + party.banquetteType.tr) }}</td>
|
|
<td>{{ party.servantRatio }}</td>
|
|
<td>{{ party.cost.toLocaleString($i18n.locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</td>
|
|
<td>{{ getPartyDate(party.createdAt).toLocaleString() }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="activeTab === 'actions'">
|
|
<p>
|
|
{{ $t('falukant.reputation.actions.description') }}
|
|
</p>
|
|
<p v-if="reputationActionsDailyCap != null" class="reputation-actions-daily">
|
|
{{ $t('falukant.reputation.actions.dailyLimit', { remaining: reputationActionsDailyRemaining, cap: reputationActionsDailyCap }) }}
|
|
</p>
|
|
|
|
<table v-if="reputationActions.length">
|
|
<thead>
|
|
<tr>
|
|
<th>{{ $t('falukant.reputation.actions.action') }}</th>
|
|
<th>{{ $t('falukant.reputation.actions.cost') }}</th>
|
|
<th>{{ $t('falukant.reputation.actions.gain') }}</th>
|
|
<th>{{ $t('falukant.reputation.actions.timesUsed') }}</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="a in reputationActions" :key="a.id">
|
|
<td>{{ $t('falukant.reputation.actions.type.' + a.tr) }}</td>
|
|
<td>{{ Number(a.cost || 0).toLocaleString($i18n.locale) }}</td>
|
|
<td>+{{ Number(a.currentGain || 0) }}</td>
|
|
<td>{{ Number(a.timesUsed || 0) }}</td>
|
|
<td>
|
|
<button type="button" :disabled="runningActionId === a.id"
|
|
@click.prevent="executeReputationAction(a)">
|
|
{{ runningActionId === a.id ? $t('falukant.reputation.actions.running') : $t('falukant.reputation.actions.execute') }}
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<p v-else>
|
|
{{ $t('falukant.reputation.actions.none') }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import StatusBar from '@/components/falukant/StatusBar.vue'
|
|
import apiClient from '@/utils/axios.js'
|
|
import Multiselect from 'vue-multiselect'
|
|
|
|
export default {
|
|
name: 'ReputationView',
|
|
components: { StatusBar, Multiselect },
|
|
data() {
|
|
return {
|
|
activeTab: 'overview',
|
|
tabs: [
|
|
{ value: 'overview', label: 'falukant.reputation.overview.title' },
|
|
{ value: 'party', label: 'falukant.reputation.party.title' },
|
|
{ value: 'actions', label: 'falukant.reputation.actions.title' }
|
|
],
|
|
newPartyView: false,
|
|
newPartyTypeId: null,
|
|
partyTypes: [],
|
|
musicId: null,
|
|
musicTypes: [],
|
|
banquetteId: null,
|
|
banquetteTypes: [],
|
|
nobilityTitles: [],
|
|
selectedNobilityIds: [],
|
|
servantRatio: 50,
|
|
inProgressParties: [],
|
|
completedParties: [],
|
|
reputation: null,
|
|
reputationActions: [],
|
|
reputationActionsDailyCap: null,
|
|
reputationActionsDailyUsed: null,
|
|
reputationActionsDailyRemaining: null,
|
|
runningActionId: null,
|
|
}
|
|
},
|
|
methods: {
|
|
toggleNewPartyView() {
|
|
this.newPartyView = !this.newPartyView
|
|
},
|
|
async loadPartyTypes() {
|
|
const { data } = await apiClient.get('/api/falukant/party/types');
|
|
this.partyTypes = data.partyTypes;
|
|
this.musicTypes = data.musicTypes;
|
|
this.banquetteTypes = data.banquetteTypes;
|
|
this.musicId = this.musicTypes[0]?.id;
|
|
this.banquetteId = this.banquetteTypes[0]?.id;
|
|
},
|
|
async loadParties() {
|
|
const { data } = await apiClient.get('/api/falukant/party');
|
|
const now = new Date();
|
|
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
|
|
this.inProgressParties = data.filter(party => {
|
|
const partyDate = this.getPartyDate(party.createdAt);
|
|
return partyDate > twentyFourHoursAgo;
|
|
});
|
|
this.completedParties = data.filter(party => {
|
|
const partyDate = this.getPartyDate(party.createdAt);
|
|
return partyDate <= twentyFourHoursAgo;
|
|
});
|
|
},
|
|
async loadReputation() {
|
|
try {
|
|
const { data } = await apiClient.get('/api/falukant/info');
|
|
this.reputation = data?.character?.reputation ?? null;
|
|
} catch (e) {
|
|
console.error('Failed to load reputation', e);
|
|
this.reputation = null;
|
|
}
|
|
},
|
|
async loadReputationActions() {
|
|
try {
|
|
const { data } = await apiClient.get('/api/falukant/reputation/actions');
|
|
this.reputationActionsDailyCap = data?.dailyCap ?? null;
|
|
this.reputationActionsDailyUsed = data?.dailyUsed ?? null;
|
|
this.reputationActionsDailyRemaining = data?.dailyRemaining ?? null;
|
|
this.reputationActions = Array.isArray(data?.actions) ? data.actions : [];
|
|
} catch (e) {
|
|
console.error('Failed to load reputation actions', e);
|
|
this.reputationActions = [];
|
|
this.reputationActionsDailyCap = null;
|
|
this.reputationActionsDailyUsed = null;
|
|
this.reputationActionsDailyRemaining = null;
|
|
}
|
|
},
|
|
async executeReputationAction(action) {
|
|
if (!action?.id) return;
|
|
if (this.runningActionId) return;
|
|
this.runningActionId = action.id;
|
|
try {
|
|
const { data } = await apiClient.post('/api/falukant/reputation/actions', { actionTypeId: action.id });
|
|
const gain = data?.gain ?? null;
|
|
const cost = data?.cost ?? null;
|
|
const msg = gain != null
|
|
? this.$t('falukant.reputation.actions.success', { gain, cost })
|
|
: this.$t('falukant.reputation.actions.successSimple');
|
|
this.$root.$refs.messageDialog?.open(msg, this.$t('falukant.reputation.actions.title'));
|
|
await Promise.all([this.loadReputation(), this.loadReputationActions()]);
|
|
} catch (e) {
|
|
const errText = e?.response?.data?.error || e?.message || String(e);
|
|
this.$root.$refs.messageDialog?.open(errText, this.$t('falukant.reputation.actions.title'));
|
|
} finally {
|
|
this.runningActionId = null;
|
|
}
|
|
},
|
|
async loadNobilityTitles() {
|
|
this.nobilityTitles = await apiClient.get('/api/falukant/nobility/titels').then(r => r.data)
|
|
},
|
|
async orderParty() {
|
|
await apiClient.post('/api/falukant/party', {
|
|
partyTypeId: this.newPartyTypeId,
|
|
musicId: this.musicId,
|
|
banquetteId: this.banquetteId,
|
|
nobilityIds: this.selectedNobilityIds.map(n => n.id ?? n),
|
|
servantRatio: this.servantRatio
|
|
});
|
|
this.toggleNewPartyView();
|
|
},
|
|
getPartyDate(createdAt) {
|
|
// Feste finden 1 Tag nach der Bestellung statt
|
|
const partyDate = new Date(createdAt);
|
|
partyDate.setDate(partyDate.getDate() + 1);
|
|
return partyDate;
|
|
}
|
|
},
|
|
computed: {
|
|
reputationDisplay() {
|
|
if (this.reputation == null) return '—';
|
|
return String(this.reputation);
|
|
},
|
|
formattedCost() {
|
|
const type = this.partyTypes.find(t => t.id === this.newPartyTypeId) || {};
|
|
const music = this.musicTypes.find(m => m.id === this.musicId) || {};
|
|
const banquette = this.banquetteTypes.find(b => b.id === this.banquetteId) || {};
|
|
let cost = (type.cost || 0) + (music.cost || 0) + (banquette.cost || 0);
|
|
cost += (50 / this.servantRatio - 1) * 1000;
|
|
let nobilityCost = this.selectedNobilityIds.reduce((sum, id) => {
|
|
const nob = this.nobilityTitles.find(n => n.id === id)
|
|
return sum + ((nob?.id ^ 5) * 1000)
|
|
}, 0);
|
|
cost += nobilityCost;
|
|
const locale = this.$i18n?.locale || 'de-DE';
|
|
return cost.toLocaleString(locale, {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
});
|
|
}
|
|
},
|
|
async mounted() {
|
|
const tabFromQuery = this.$route?.query?.tab;
|
|
if (['overview','party','actions'].includes(tabFromQuery)) {
|
|
this.activeTab = tabFromQuery;
|
|
}
|
|
await this.loadPartyTypes();
|
|
await this.loadNobilityTitles();
|
|
await this.loadParties();
|
|
await this.loadReputation();
|
|
await this.loadReputationActions();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
h2 {
|
|
padding-top: 20px;
|
|
}
|
|
|
|
.simple-tabs {
|
|
display: flex;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.simple-tab {
|
|
padding: 0.5rem 1rem;
|
|
background: #fff;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.simple-tab.active {
|
|
background: #F9A22C;
|
|
color: #000;
|
|
}
|
|
|
|
.tab-content {
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.new-party-form {
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.party-options {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.total-cost {
|
|
font-weight: bold;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.multiselect {
|
|
display: inline-block !important;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
table th {
|
|
text-align: left;
|
|
}
|
|
|
|
.separator-class {
|
|
border-top: 1px solid #ccc;
|
|
margin-top: 1em;
|
|
}
|
|
|
|
.reputation-actions-daily {
|
|
margin: 0.5rem 0 1rem;
|
|
font-weight: bold;
|
|
}
|
|
</style> |