Add servant management features: Implement endpoints for hiring, dismissing, and setting pay levels for servants in the FalukantController. Update UserHouse model to include servant-related attributes. Enhance frontend components to manage servant details, including staffing state and household order, with corresponding localization updates in multiple languages.

This commit is contained in:
Torsten Schulz (local)
2026-03-22 09:57:44 +01:00
parent 2977b152a2
commit 876ee2ab49
12 changed files with 1661 additions and 17 deletions

View File

@@ -34,6 +34,65 @@
</div>
</div>
<section v-if="userHouse" class="servants-panel surface-card">
<div class="servants-panel__header">
<div>
<h3>{{ $t('falukant.house.servants.title') }}</h3>
<p>{{ $t('falukant.house.servants.description') }}</p>
</div>
<div class="servants-panel__actions">
<button @click="hireServant">
{{ $t('falukant.house.servants.actions.hire') }}
</button>
<button class="button-secondary" :disabled="(userHouse.servantCount || 0) <= 0" @click="dismissServant">
{{ $t('falukant.house.servants.actions.dismiss') }}
</button>
</div>
</div>
<div class="servants-grid">
<article class="servant-card">
<span class="servant-card__label">{{ $t('falukant.house.servants.count') }}</span>
<strong>{{ userHouse.servantCount || 0 }}</strong>
</article>
<article class="servant-card">
<span class="servant-card__label">{{ $t('falukant.house.servants.expectedRange') }}</span>
<strong>{{ servantSummary.expectedMin }} - {{ servantSummary.expectedMax }}</strong>
</article>
<article class="servant-card">
<span class="servant-card__label">{{ $t('falukant.house.servants.monthlyCost') }}</span>
<strong>{{ formatPrice(servantSummary.monthlyCost || 0) }} {{ currency }}</strong>
</article>
<article class="servant-card">
<span class="servant-card__label">{{ $t('falukant.house.servants.quality') }}</span>
<strong>{{ userHouse.servantQuality || 0 }}</strong>
</article>
<article class="servant-card">
<span class="servant-card__label">{{ $t('falukant.house.servants.householdOrder') }}</span>
<strong>{{ userHouse.householdOrder || 0 }}</strong>
</article>
<article class="servant-card">
<span class="servant-card__label">{{ $t('falukant.house.servants.staffingState.label') }}</span>
<strong>{{ $t(`falukant.house.servants.staffingState.${servantSummary.staffingState || 'fitting'}`) }}</strong>
</article>
</div>
<div class="servants-settings">
<label class="servants-settings__label">
{{ $t('falukant.house.servants.payLevel') }}
<select v-model="servantPayLevel" @change="updateServantPayLevel" class="servants-settings__select">
<option v-for="option in servantPayOptions" :key="option" :value="option">
{{ $t(`falukant.house.servants.payLevels.${option}`) }}
</option>
</select>
</label>
<div class="servants-settings__state">
{{ $t('falukant.house.servants.orderState.label') }}:
<strong>{{ $t(`falukant.house.servants.orderState.${servantSummary.orderState || 'stable'}`) }}</strong>
</div>
</div>
</section>
<div class="buyable-houses">
<h3>{{ $t('falukant.house.buyablehouses') }}</h3>
<div class="houses-list">
@@ -67,6 +126,7 @@
import StatusBar from '@/components/falukant/StatusBar.vue';
import apiClient from '@/utils/axios.js';
import { mapState } from 'vuex';
import { showError, showSuccess, confirmAction } from '@/utils/feedback.js';
export default {
name: 'HouseView',
@@ -76,6 +136,15 @@ export default {
userHouse: null,
houseType: {},
status: {},
servantSummary: {
expectedMin: 0,
expectedMax: 0,
monthlyCost: 0,
staffingState: 'fitting',
orderState: 'stable'
},
servantPayLevel: 'normal',
servantPayOptions: ['low', 'normal', 'high'],
buyableHouses: [],
currency: '€'
};
@@ -94,6 +163,8 @@ export default {
this.houseType = this.userHouse.houseType;
const { roofCondition, wallCondition, floorCondition, windowCondition } = this.userHouse;
this.status = { roofCondition, wallCondition, floorCondition, windowCondition };
this.servantSummary = this.userHouse.servantSummary || this.servantSummary;
this.servantPayLevel = this.userHouse.servantPayLevel || 'normal';
const buyRes = await apiClient.get('/api/falukant/houses/buyable');
this.buyableHouses = buyRes.data;
@@ -172,19 +243,60 @@ export default {
}
},
async sellHouse() {
const confirmed = await confirmAction(this, {
title: this.$t('falukant.house.sell'),
text: this.$t('falukant.house.sellConfirm')
});
if (!confirmed) return;
try {
await apiClient.post('/api/falukant/houses/sell');
await this.loadData();
showSuccess(this, this.$t('falukant.house.sellSuccess'));
} catch (err) {
console.error('Error selling house', err);
showError(this, this.$t('falukant.house.sellError'));
}
},
async buyHouse(id) {
try {
await apiClient.post('/api/falukant/houses', { houseId: id });
await this.loadData();
showSuccess(this, this.$t('falukant.house.buySuccess'));
} catch (err) {
console.error('Error buying house', err);
showError(this, this.$t('falukant.house.buyError'));
}
},
async hireServant() {
try {
await apiClient.post('/api/falukant/houses/servants/hire', { amount: 1 });
await this.loadData();
showSuccess(this, this.$t('falukant.house.servants.actions.hireSuccess'));
} catch (err) {
console.error('Error hiring servant', err);
showError(this, this.$t('falukant.house.servants.actions.hireError'));
}
},
async dismissServant() {
try {
await apiClient.post('/api/falukant/houses/servants/dismiss', { amount: 1 });
await this.loadData();
showSuccess(this, this.$t('falukant.house.servants.actions.dismissSuccess'));
} catch (err) {
console.error('Error dismissing servant', err);
showError(this, this.$t('falukant.house.servants.actions.dismissError'));
}
},
async updateServantPayLevel() {
try {
await apiClient.post('/api/falukant/houses/servants/pay-level', {
payLevel: this.servantPayLevel
});
await this.loadData();
showSuccess(this, this.$t('falukant.house.servants.actions.payLevelSuccess'));
} catch (err) {
console.error('Error updating servant pay level', err);
showError(this, this.$t('falukant.house.servants.actions.payLevelError'));
}
},
handleDaemonMessage(evt) {
@@ -258,6 +370,78 @@ h2 {
padding: 18px;
}
.servants-panel {
padding: 18px;
}
.servants-panel__header {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-start;
margin-bottom: 16px;
}
.servants-panel__header h3 {
margin: 0 0 6px;
}
.servants-panel__header p {
margin: 0;
color: var(--color-text-secondary);
}
.servants-panel__actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.servants-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.servant-card {
display: flex;
flex-direction: column;
gap: 6px;
padding: 14px 16px;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.68);
}
.servant-card__label {
color: var(--color-text-secondary);
font-size: 0.88rem;
}
.servants-settings {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.servants-settings__label {
display: flex;
flex-direction: column;
gap: 6px;
font-weight: 600;
}
.servants-settings__select {
min-width: 200px;
}
.servants-settings__state {
color: var(--color-text-secondary);
}
.buyable-houses {
display: flex;
flex-direction: column;
@@ -356,6 +540,10 @@ button {
flex-direction: column;
}
.servants-panel__header {
flex-direction: column;
}
.house {
width: min(341px, 100%);
margin: 0 auto;