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

@@ -826,8 +826,51 @@
"price": "Kaufpreis",
"worth": "Restwert",
"sell": "Verkaufen",
"sellConfirm": "Möchtest du dein Haus wirklich verkaufen?",
"sellSuccess": "Das Haus wurde verkauft.",
"sellError": "Das Haus konnte nicht verkauft werden.",
"buySuccess": "Das Haus wurde gekauft.",
"buyError": "Das Haus konnte nicht gekauft werden.",
"renovate": "Renovieren",
"renovateAll": "Komplett renovieren",
"servants": {
"title": "Dienerschaft",
"description": "Verwalte Hauspersonal, Ordnung und laufende Kosten deines Haushalts.",
"count": "Dienerzahl",
"expectedRange": "Erwarteter Bereich",
"monthlyCost": "Monatskosten",
"quality": "Qualität",
"householdOrder": "Haushaltsordnung",
"payLevel": "Bezahlung",
"payLevels": {
"low": "Niedrig",
"normal": "Normal",
"high": "Großzügig"
},
"staffingState": {
"label": "Besetzung",
"understaffed": "Unterbesetzt",
"fitting": "Passend",
"overstaffed": "Überbesetzt"
},
"orderState": {
"label": "Ordnungszustand",
"chaotic": "Chaotisch",
"strained": "Angespannt",
"stable": "Stabil",
"excellent": "Vorbildlich"
},
"actions": {
"hire": "1 Diener einstellen",
"dismiss": "1 Diener entlassen",
"hireSuccess": "Die Dienerschaft wurde erweitert.",
"hireError": "Die Dienerschaft konnte nicht erweitert werden.",
"dismissSuccess": "Ein Diener wurde entlassen.",
"dismissError": "Der Diener konnte nicht entlassen werden.",
"payLevelSuccess": "Die Bezahlung der Dienerschaft wurde angepasst.",
"payLevelError": "Die Bezahlung konnte nicht angepasst werden."
}
},
"status": {
"roofCondition": "Dach",
"wallCondition": "Wände",

View File

@@ -149,7 +149,7 @@
"all": "All history"
}
},
"activities": {
"activities": {
"Product sale": "Product sale",
"Production cost": "Production cost",
"Sell all products": "Sell all products",
@@ -181,6 +181,75 @@
}
}
},
"house": {
"title": "House",
"statusreport": "House condition",
"element": "Element",
"state": "Condition",
"buyablehouses": "Buy a house",
"buy": "Buy",
"price": "Purchase price",
"worth": "Residual value",
"sell": "Sell",
"sellConfirm": "Do you really want to sell your house?",
"sellSuccess": "The house has been sold.",
"sellError": "The house could not be sold.",
"buySuccess": "The house has been bought.",
"buyError": "The house could not be bought.",
"renovate": "Renovate",
"renovateAll": "Renovate completely",
"servants": {
"title": "Servants",
"description": "Manage household staff, order and recurring costs in your home.",
"count": "Servant count",
"expectedRange": "Expected range",
"monthlyCost": "Monthly cost",
"quality": "Quality",
"householdOrder": "Household order",
"payLevel": "Pay level",
"payLevels": {
"low": "Low",
"normal": "Normal",
"high": "Generous"
},
"staffingState": {
"label": "Staffing",
"understaffed": "Understaffed",
"fitting": "Fitting",
"overstaffed": "Overstaffed"
},
"orderState": {
"label": "Order state",
"chaotic": "Chaotic",
"strained": "Strained",
"stable": "Stable",
"excellent": "Excellent"
},
"actions": {
"hire": "Hire 1 servant",
"dismiss": "Dismiss 1 servant",
"hireSuccess": "The household staff has been expanded.",
"hireError": "The staff could not be expanded.",
"dismissSuccess": "A servant has been dismissed.",
"dismissError": "The servant could not be dismissed.",
"payLevelSuccess": "Servant pay has been updated.",
"payLevelError": "Servant pay could not be updated."
}
},
"status": {
"roofCondition": "Roof",
"wallCondition": "Walls",
"floorCondition": "Floors",
"windowCondition": "Windows"
},
"type": {
"backyard_room": "Backyard room",
"wooden_house": "Wooden house",
"straw_hut": "Straw hut",
"family_house": "Family house",
"townhouse": "Townhouse"
}
},
"newdirector": {
"title": "New Director",
"age": "Age",

View File

@@ -792,8 +792,51 @@
"price": "Precio de compra",
"worth": "Valor restante",
"sell": "Vender",
"sellConfirm": "¿De verdad quieres vender tu casa?",
"sellSuccess": "La casa ha sido vendida.",
"sellError": "No se pudo vender la casa.",
"buySuccess": "La casa ha sido comprada.",
"buyError": "No se pudo comprar la casa.",
"renovate": "Renovar",
"renovateAll": "Renovar por completo",
"servants": {
"title": "Servicio doméstico",
"description": "Administra el personal, el orden y los costes periódicos de tu casa.",
"count": "Número de sirvientes",
"expectedRange": "Rango esperado",
"monthlyCost": "Coste mensual",
"quality": "Calidad",
"householdOrder": "Orden del hogar",
"payLevel": "Pago",
"payLevels": {
"low": "Bajo",
"normal": "Normal",
"high": "Generoso"
},
"staffingState": {
"label": "Dotación",
"understaffed": "Insuficiente",
"fitting": "Adecuada",
"overstaffed": "Excesiva"
},
"orderState": {
"label": "Estado del orden",
"chaotic": "Caótico",
"strained": "Tenso",
"stable": "Estable",
"excellent": "Excelente"
},
"actions": {
"hire": "Contratar 1 sirviente",
"dismiss": "Despedir 1 sirviente",
"hireSuccess": "Se ha ampliado el servicio doméstico.",
"hireError": "No se pudo ampliar el servicio doméstico.",
"dismissSuccess": "Se ha despedido a un sirviente.",
"dismissError": "No se pudo despedir al sirviente.",
"payLevelSuccess": "Se ha ajustado el pago del servicio.",
"payLevelError": "No se pudo ajustar el pago."
}
},
"status": {
"roofCondition": "Techo",
"wallCondition": "Paredes",

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;