Add marriage management features: Implement endpoints for spending time with, gifting to, and reconciling with spouses in the FalukantController. Update UserHouse model to include household tension attributes. Enhance frontend components to manage marriage actions and display household tension details, including localization updates in multiple languages.

This commit is contained in:
Torsten Schulz (local)
2026-03-23 09:34:56 +01:00
parent 2055c11fd9
commit f7e0d97174
23 changed files with 1997 additions and 52 deletions

View File

@@ -108,7 +108,7 @@ export default {
},
setupSocketListeners() {
this.teardownSocketListeners();
const daemonEvents = ['falukantUpdateStatus', 'falukantUpdateFamily', 'children_update', 'stock_change', 'familychanged'];
const daemonEvents = ['falukantUpdateStatus', 'falukantUpdateFamily', 'children_update', 'falukantUpdateProductionCertificate', 'stock_change', 'familychanged'];
if (this.daemonSocket) {
this._daemonMessageHandler = (event) => {
if (event.data === 'ping') return;
@@ -129,11 +129,15 @@ export default {
this._childrenSocketHandler = (data) => {
if (this.matchesCurrentUser(data)) this.queueFetchData();
};
this._productionCertificateSocketHandler = (data) => {
if (this.matchesCurrentUser(data)) this.queueFetchData();
};
this._branchSocketHandler = () => this.queueFetchData();
this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
this.socket.on('falukantUpdateFamily', this._familySocketHandler);
this.socket.on('children_update', this._childrenSocketHandler);
this.socket.on('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
this.socket.on('falukantBranchUpdate', this._branchSocketHandler);
}
},
@@ -146,6 +150,7 @@ export default {
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler);
if (this._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler);
if (this._productionCertificateSocketHandler) this.socket.off('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
if (this._branchSocketHandler) this.socket.off('falukantBranchUpdate', this._branchSocketHandler);
}
},

View File

@@ -177,12 +177,14 @@ export default {
this._statusSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data });
this._familySocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateFamily', ...data });
this._childrenSocketHandler = (data) => this.handleEvent({ event: 'children_update', ...data });
this._productionCertificateSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateProductionCertificate', ...data });
this._stockSocketHandler = (data) => this.handleEvent({ event: 'stock_change', ...data });
this._familyChangedSocketHandler = (data) => this.handleEvent({ event: 'familychanged', ...data });
this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
this.socket.on('falukantUpdateFamily', this._familySocketHandler);
this.socket.on('children_update', this._childrenSocketHandler);
this.socket.on('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
this.socket.on('stock_change', this._stockSocketHandler);
this.socket.on('familychanged', this._familyChangedSocketHandler);
},
@@ -191,6 +193,7 @@ export default {
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler);
if (this._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler);
if (this._productionCertificateSocketHandler) this.socket.off('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
if (this._stockSocketHandler) this.socket.off('stock_change', this._stockSocketHandler);
if (this._familyChangedSocketHandler) this.socket.off('familychanged', this._familyChangedSocketHandler);
}
@@ -201,7 +204,7 @@ export default {
this._daemonHandler = (event) => {
try {
const data = JSON.parse(event.data);
if (['falukantUpdateStatus', 'falukantUpdateFamily', 'children_update', 'stock_change', 'familychanged'].includes(data.event)) {
if (['falukantUpdateStatus', 'falukantUpdateFamily', 'children_update', 'falukantUpdateProductionCertificate', 'stock_change', 'familychanged'].includes(data.event)) {
this.handleEvent(data);
}
} catch (_) {}
@@ -241,6 +244,7 @@ export default {
case 'falukantUpdateStatus':
case 'falukantUpdateFamily':
case 'children_update':
case 'falukantUpdateProductionCertificate':
case 'stock_change':
case 'familychanged':
this.queueStatusRefresh();

View File

@@ -235,7 +235,7 @@ export default {
});
},
async fetchImage(image) {
const userId = localStorage.getItem('userid');
const userId = localStorage.getItem('userid') || sessionStorage.getItem('userid');
try {
const response = await apiClient.get(`/api/socialnetwork/image/${image.hash}`, {
headers: {

View File

@@ -130,7 +130,8 @@
"years": "Jahre",
"days": "Tage",
"mainbranch": "Heimatstadt",
"nobleTitle": "Stand"
"nobleTitle": "Stand",
"certificate": "Zertifikat"
},
"productions": {
"title": "Produktionen"
@@ -219,6 +220,7 @@
},
"branch": {
"title": "Filiale",
"currentCertificate": "Derzeitiges Zertifikat",
"tabs": {
"director": "Direktor",
"inventory": "Inventar",
@@ -526,9 +528,34 @@
},
"householdTension": {
"label": "Hausfrieden",
"score": "Spannungswert",
"reasonsLabel": "Aktuelle Ursachen",
"low": "Ruhig",
"medium": "Unruhig",
"high": "Belastet"
"high": "Belastet",
"reasons": {
"visibleLover": "Sichtbare Liebschaft",
"noticeableLover": "Auffällige Liebschaft",
"underfundedLover": "Unterversorgte Liebschaft",
"acknowledgedAffair": "Anerkannte Liebschaft",
"statusMismatch": "Standesunterschied",
"loverChild": "Kind aus Liebschaft",
"disorder": "Unordnung im Haus",
"tooFewServants": "Zu wenig Diener",
"marriageCrisis": "Ehekrise"
}
},
"marriageActions": {
"title": "Ehe pflegen",
"spendTime": "Zeit miteinander verbringen",
"giftSmall": "Kleines Geschenk",
"giftDecent": "Gutes Geschenk",
"giftLavish": "Großzügiges Geschenk",
"reconcile": "Streit schlichten",
"spendTimeSuccess": "Die gemeinsame Zeit hat die Ehe stabilisiert.",
"giftSuccess": "Das Geschenk hat die Ehe verbessert.",
"reconcileSuccess": "Der Streit wurde fürs Erste geschlichtet.",
"actionError": "Die Aktion konnte nicht ausgeführt werden."
},
"relationships": {
"name": "Name"
@@ -863,12 +890,15 @@
"actions": {
"hire": "1 Diener einstellen",
"dismiss": "1 Diener entlassen",
"tidy": "Haus ordnen",
"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."
"payLevelError": "Die Bezahlung konnte nicht angepasst werden.",
"tidySuccess": "Das Haus wurde geordnet.",
"tidyError": "Das Haus konnte nicht geordnet werden."
}
},
"status": {

View File

@@ -111,7 +111,8 @@
"years": "Years",
"days": "Days",
"mainbranch": "Home city",
"nobleTitle": "Title"
"nobleTitle": "Title",
"certificate": "Certificate"
}
},
"health": {
@@ -228,12 +229,15 @@
"actions": {
"hire": "Hire 1 servant",
"dismiss": "Dismiss 1 servant",
"tidy": "Tidy household",
"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."
"payLevelError": "Servant pay could not be updated.",
"tidySuccess": "The household has been put in order.",
"tidyError": "The household could not be put in order."
}
},
"status": {
@@ -261,6 +265,7 @@
"noProposals": "No director candidates available."
},
"branch": {
"currentCertificate": "Current certificate",
"selection": {
"title": "Branch Selection",
"selected": "Selected Branch",
@@ -540,9 +545,34 @@
},
"householdTension": {
"label": "Household Tension",
"score": "Tension score",
"reasonsLabel": "Current causes",
"low": "Calm",
"medium": "Uneasy",
"high": "Strained"
"high": "Strained",
"reasons": {
"visibleLover": "Visible affair",
"noticeableLover": "Noticeable affair",
"underfundedLover": "Underfunded affair",
"acknowledgedAffair": "Acknowledged affair",
"statusMismatch": "Status mismatch",
"loverChild": "Child from an affair",
"disorder": "Disorder in the house",
"tooFewServants": "Too few servants",
"marriageCrisis": "Marriage crisis"
}
},
"marriageActions": {
"title": "Support the marriage",
"spendTime": "Spend time together",
"giftSmall": "Small gift",
"giftDecent": "Decent gift",
"giftLavish": "Lavish gift",
"reconcile": "Reconcile dispute",
"spendTimeSuccess": "The time together has stabilized the marriage.",
"giftSuccess": "The gift has improved the marriage.",
"reconcileSuccess": "The dispute has been eased for now.",
"actionError": "The action could not be completed."
},
"lovers": {
"title": "Lovers and Mistresses",

View File

@@ -120,7 +120,8 @@
"age": "Edad",
"years": "años",
"mainbranch": "Ciudad natal",
"nobleTitle": "Rango"
"nobleTitle": "Rango",
"certificate": "Certificado"
},
"productions": {
"title": "Producciones"
@@ -207,6 +208,7 @@
},
"branch": {
"title": "Sucursal",
"currentCertificate": "Certificado actual",
"tabs": {
"director": "Director",
"inventory": "Inventario",
@@ -510,9 +512,34 @@
},
"householdTension": {
"label": "Tensión del hogar",
"score": "Valor de tensión",
"reasonsLabel": "Causas actuales",
"low": "Calmo",
"medium": "Inquieto",
"high": "Tenso"
"high": "Tenso",
"reasons": {
"visibleLover": "Relación visible",
"noticeableLover": "Relación llamativa",
"underfundedLover": "Relación infrafinanciada",
"acknowledgedAffair": "Relación reconocida",
"statusMismatch": "Desajuste social",
"loverChild": "Hijo de una relación",
"disorder": "Desorden en la casa",
"tooFewServants": "Muy pocos sirvientes",
"marriageCrisis": "Crisis matrimonial"
}
},
"marriageActions": {
"title": "Cuidar el matrimonio",
"spendTime": "Pasar tiempo juntos",
"giftSmall": "Regalo pequeño",
"giftDecent": "Buen regalo",
"giftLavish": "Regalo generoso",
"reconcile": "Resolver disputa",
"spendTimeSuccess": "El tiempo compartido ha estabilizado el matrimonio.",
"giftSuccess": "El regalo ha mejorado el matrimonio.",
"reconcileSuccess": "La disputa se ha calmado por ahora.",
"actionError": "No se pudo realizar la acción."
},
"relationships": {
"name": "Nombre"
@@ -829,12 +856,15 @@
"actions": {
"hire": "Contratar 1 sirviente",
"dismiss": "Despedir 1 sirviente",
"tidy": "Ordenar la casa",
"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."
"payLevelError": "No se pudo ajustar el pago.",
"tidySuccess": "La casa ha sido ordenada.",
"tidyError": "No se pudo ordenar la casa."
}
},
"status": {

View File

@@ -6,12 +6,44 @@ import apiClient from '../utils/axios.js';
import { io } from 'socket.io-client';
import { getDaemonSocketUrl, getSocketIoUrl } from '../utils/appConfig.js';
const AUTH_KEYS = ['isLoggedIn', 'user', 'userid'];
function getStoredValue(key) {
return localStorage.getItem(key) ?? sessionStorage.getItem(key);
}
function getStoredUser() {
const storedUser = getStoredValue('user');
if (!storedUser) return null;
try {
return JSON.parse(storedUser);
} catch (_) {
return null;
}
}
function clearAuthStorage() {
AUTH_KEYS.forEach((key) => {
localStorage.removeItem(key);
sessionStorage.removeItem(key);
});
}
function persistAuthStorage(user, rememberMe) {
const targetStorage = rememberMe ? localStorage : sessionStorage;
clearAuthStorage();
targetStorage.setItem('isLoggedIn', 'true');
targetStorage.setItem('user', JSON.stringify(user));
targetStorage.setItem('userid', user?.id || '');
}
const store = createStore({
state: {
isLoggedIn: localStorage.getItem('isLoggedIn') === 'true',
isLoggedIn: getStoredValue('isLoggedIn') === 'true',
connectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error'
daemonConnectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error'
user: JSON.parse(localStorage.getItem('user')) || null,
user: getStoredUser(),
// Reconnect state management
backendRetryCount: 0,
daemonRetryCount: 0,
@@ -52,11 +84,12 @@ const store = createStore({
menuNeedsUpdate: false,
},
mutations: {
async dologin(state, user) {
async dologin(state, payload) {
const loginPayload = payload?.user ? payload : { user: payload, rememberMe: true };
const { user, rememberMe = true } = loginPayload;
state.isLoggedIn = true;
state.user = user;
localStorage.setItem('isLoggedIn', 'true');
localStorage.setItem('user', JSON.stringify(user));
persistAuthStorage(user, rememberMe);
state.menuNeedsUpdate = true;
if (user.param.filter(param => ['birthdate', 'gender'].includes(param.name)).length < 2) {
router.push({ path: '/settings/personal' });
@@ -65,8 +98,7 @@ const store = createStore({
async dologout(state) {
state.isLoggedIn = false;
state.user = null;
localStorage.removeItem('isLoggedIn');
localStorage.removeItem('user');
clearAuthStorage();
localStorage.removeItem('menu');
state.menuNeedsUpdate = false;
@@ -145,8 +177,8 @@ const store = createStore({
},
},
actions: {
async login({ commit, dispatch }, user) {
await commit('dologin', user);
async login({ commit, dispatch }, payload) {
await commit('dologin', payload);
await dispatch('initializeSocket');
await dispatch('initializeDaemonSocket');
const socket = this.getters.socket;

View File

@@ -7,6 +7,11 @@
<span class="branch-kicker">Niederlassung</span>
<h2>{{ $t('falukant.branch.title') }}</h2>
<p>Produktion, Lager, Verkauf und Transport in einer spielweltbezogenen Steuerflaeche.</p>
<div class="branch-hero__meta">
<span class="branch-hero__badge">
{{ $t('falukant.branch.currentCertificate') }}: {{ currentCertificate ?? '---' }}
</span>
</div>
</div>
</section>
@@ -398,11 +403,13 @@ export default {
branchTaxes: null,
branchTaxesLoading: false,
branchTaxesError: null,
currentCertificate: null,
pendingBranchRefresh: null,
};
},
computed: {
...mapState(['socket', 'daemonSocket']),
...mapState(['socket', 'daemonSocket', 'user']),
freeVehiclesByType() {
const grouped = {};
for (const v of this.vehicles || []) {
@@ -436,6 +443,7 @@ export default {
await this.loadBranches();
const branchId = this.$route.params.branchId;
await this.loadCurrentCertificate();
await this.loadProducts();
if (branchId) {
@@ -454,6 +462,7 @@ export default {
// Live-Socket-Events (Backend Socket.io)
if (this.socket) {
this.socket.on('falukantUpdateStatus', (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data }));
this.socket.on('falukantUpdateProductionCertificate', (data) => this.handleEvent({ event: 'falukantUpdateProductionCertificate', ...data }));
this.socket.on('falukantBranchUpdate', (data) => this.handleEvent({ event: 'falukantBranchUpdate', ...data }));
this.socket.on('transport_arrived', (data) => this.handleEvent({ event: 'transport_arrived', ...data }));
this.socket.on('inventory_updated', (data) => this.handleEvent({ event: 'inventory_updated', ...data }));
@@ -463,12 +472,17 @@ export default {
},
beforeUnmount() {
if (this.pendingBranchRefresh) {
clearTimeout(this.pendingBranchRefresh);
this.pendingBranchRefresh = null;
}
// Daemon WebSocket: Listener entfernen (der Socket selbst wird beim Logout geschlossen)
if (this.daemonSocket) {
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
}
if (this.socket) {
this.socket.off('falukantUpdateStatus');
this.socket.off('falukantUpdateProductionCertificate');
this.socket.off('falukantBranchUpdate');
this.socket.off('transport_arrived');
this.socket.off('inventory_updated');
@@ -493,6 +507,34 @@ export default {
},
methods: {
matchesCurrentUser(eventData) {
if (eventData?.user_id == null) {
return true;
}
const currentIds = [this.user?.id, this.user?.hashedId]
.filter(Boolean)
.map((value) => String(value));
return currentIds.includes(String(eventData.user_id));
},
queueBranchRefresh() {
if (this.pendingBranchRefresh) {
clearTimeout(this.pendingBranchRefresh);
}
this.pendingBranchRefresh = setTimeout(async () => {
this.pendingBranchRefresh = null;
this.$refs.statusBar?.fetchStatus();
await this.loadCurrentCertificate();
await this.loadProducts();
this.$refs.productionSection?.loadProductions();
this.$refs.productionSection?.loadStorage();
this.$refs.storageSection?.loadStorageData();
this.$refs.saleSection?.loadInventory();
if (this.$refs.revenueSection) {
this.$refs.revenueSection.products = this.products;
this.$refs.revenueSection.refresh && this.$refs.revenueSection.refresh();
}
}, 120);
},
async loadBranches() {
try {
const result = await apiClient.get('/api/falukant/branches');
@@ -512,6 +554,14 @@ export default {
console.error('Error loading branches:', error);
}
},
async loadCurrentCertificate() {
try {
const result = await apiClient.get('/api/falukant/user');
this.currentCertificate = result.data?.certificate ?? null;
} catch (error) {
console.error('Error loading certificate:', error);
}
},
async loadProducts() {
try {
@@ -771,6 +821,9 @@ export default {
},
handleEvent(eventData) {
if (!this.matchesCurrentUser(eventData)) {
return;
}
switch (eventData.event) {
case 'production_ready':
this.$refs.productionSection?.loadProductions();
@@ -798,30 +851,12 @@ export default {
this.$refs.productionSection?.loadStorage();
break;
case 'falukantUpdateStatus':
case 'falukantUpdateProductionCertificate':
case 'falukantBranchUpdate':
if (this.$refs.statusBar) {
this.$refs.statusBar.fetchStatus();
}
if (this.$refs.productionSection) {
this.$refs.productionSection.loadProductions();
this.$refs.productionSection.loadStorage();
}
if (this.$refs.storageSection) {
this.$refs.storageSection.loadStorageData();
}
if (this.$refs.saleSection) {
this.$refs.saleSection.loadInventory();
}
this.queueBranchRefresh();
break;
case 'knowledge_update':
this.loadProducts();
if (this.$refs.revenueSection) {
this.$refs.revenueSection.products = this.products;
this.$refs.revenueSection.refresh && this.$refs.revenueSection.refresh();
}
this.queueBranchRefresh();
break;
case 'transport_arrived':
// Leerer Transport angekommen - Fahrzeug wurde zurückgeholt
@@ -1149,6 +1184,22 @@ export default {
color: var(--color-text-secondary);
}
.branch-hero__meta {
margin-top: 12px;
}
.branch-hero__badge {
display: inline-flex;
align-items: center;
padding: 6px 10px;
border: 1px solid rgba(138, 84, 17, 0.16);
border-radius: 999px;
background: rgba(255, 255, 255, 0.7);
color: #7a4b12;
font-size: 0.9rem;
font-weight: 600;
}
.branch-tab-content {
margin-top: 16px;
padding: 18px;

View File

@@ -149,6 +149,39 @@
{{ $t('falukant.family.householdTension.' + householdTension) }}
</span>
</div>
<div class="marriage-overview__item" v-if="householdTensionScore != null">
<span class="marriage-overview__label">{{ $t('falukant.family.householdTension.score') }}</span>
<strong>{{ householdTensionScore }}</strong>
</div>
</section>
<section v-if="relationships.length > 0 && relationships[0].relationshipType === 'married'" class="marriage-actions surface-card">
<h3>{{ $t('falukant.family.marriageActions.title') }}</h3>
<div class="marriage-actions__buttons">
<button class="button button--secondary" @click="spendTimeWithSpouse">
{{ $t('falukant.family.marriageActions.spendTime') }}
</button>
<button class="button button--secondary" @click="giftToSpouse('small')">
{{ $t('falukant.family.marriageActions.giftSmall') }}
</button>
<button class="button button--secondary" @click="giftToSpouse('decent')">
{{ $t('falukant.family.marriageActions.giftDecent') }}
</button>
<button class="button button--secondary" @click="giftToSpouse('lavish')">
{{ $t('falukant.family.marriageActions.giftLavish') }}
</button>
<button class="button button--secondary" @click="reconcileMarriage">
{{ $t('falukant.family.marriageActions.reconcile') }}
</button>
</div>
<div v-if="householdTensionReasons.length > 0" class="marriage-actions__reasons">
<span class="marriage-actions__reasons-label">{{ $t('falukant.family.householdTension.reasonsLabel') }}</span>
<div class="marriage-actions__reason-list">
<span v-for="reason in householdTensionReasons" :key="reason" class="lover-meta-badge lover-meta-badge--warning">
{{ $t('falukant.family.householdTension.reasons.' + reason) }}
</span>
</div>
</div>
</section>
<div class="children-section">
@@ -351,6 +384,8 @@ export default {
marriageSatisfaction: null,
marriageState: null,
householdTension: null,
householdTensionScore: null,
householdTensionReasons: [],
selectedChild: null,
pendingFamilyRefresh: null
}
@@ -495,11 +530,46 @@ export default {
this.marriageSatisfaction = response.data.marriageSatisfaction;
this.marriageState = response.data.marriageState;
this.householdTension = response.data.householdTension;
this.householdTensionScore = response.data.householdTensionScore;
this.householdTensionReasons = response.data.householdTensionReasons || [];
} catch (error) {
console.error('Error loading family data:', error);
}
},
async spendTimeWithSpouse() {
try {
await apiClient.post('/api/falukant/family/marriage/spend-time');
await this.loadFamilyData();
showSuccess(this, this.$t('falukant.family.marriageActions.spendTimeSuccess'));
} catch (error) {
console.error('Error spending time with spouse:', error);
showError(this, this.$t('falukant.family.marriageActions.actionError'));
}
},
async giftToSpouse(giftLevel) {
try {
await apiClient.post('/api/falukant/family/marriage/gift', { giftLevel });
await this.loadFamilyData();
showSuccess(this, this.$t('falukant.family.marriageActions.giftSuccess'));
} catch (error) {
console.error('Error gifting spouse:', error);
showError(this, this.$t('falukant.family.marriageActions.actionError'));
}
},
async reconcileMarriage() {
try {
await apiClient.post('/api/falukant/family/marriage/reconcile');
await this.loadFamilyData();
showSuccess(this, this.$t('falukant.family.marriageActions.reconcileSuccess'));
} catch (error) {
console.error('Error reconciling marriage:', error);
showError(this, this.$t('falukant.family.marriageActions.actionError'));
}
},
async loadOwnCharacter() {
try {
const response = await apiClient.get('/api/falukant/user');
@@ -814,6 +884,39 @@ export default {
font-size: 0.88rem;
}
.marriage-actions {
display: grid;
gap: 12px;
margin-bottom: 18px;
padding: 16px 18px;
}
.marriage-actions h3 {
margin: 0;
}
.marriage-actions__buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.marriage-actions__reasons {
display: grid;
gap: 8px;
}
.marriage-actions__reasons-label {
color: var(--color-text-secondary);
font-size: 0.88rem;
}
.marriage-actions__reason-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.inline-status-pill {
display: inline-flex;
align-items: center;

View File

@@ -47,6 +47,9 @@
<button class="button-secondary" :disabled="(userHouse.servantCount || 0) <= 0" @click="dismissServant">
{{ $t('falukant.house.servants.actions.dismiss') }}
</button>
<button class="button-secondary" @click="tidyHousehold">
{{ $t('falukant.house.servants.actions.tidy') }}
</button>
</div>
</div>
@@ -71,6 +74,10 @@
<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.family.householdTension.score') }}</span>
<strong>{{ userHouse.householdTensionScore ?? 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>
@@ -91,6 +98,14 @@
<strong>{{ $t(`falukant.house.servants.orderState.${servantSummary.orderState || 'stable'}`) }}</strong>
</div>
</div>
<div v-if="Array.isArray(userHouse.householdTensionReasonsJson) && userHouse.householdTensionReasonsJson.length > 0" class="servants-reasons">
<span class="servants-reasons__label">{{ $t('falukant.family.householdTension.reasonsLabel') }}</span>
<div class="servants-reasons__list">
<span v-for="reason in userHouse.householdTensionReasonsJson" :key="reason" class="servants-reasons__badge">
{{ $t('falukant.family.householdTension.reasons.' + reason) }}
</span>
</div>
</div>
</section>
<div class="buyable-houses">
@@ -299,6 +314,16 @@ export default {
showError(this, this.$t('falukant.house.servants.actions.payLevelError'));
}
},
async tidyHousehold() {
try {
await apiClient.post('/api/falukant/houses/order');
await this.loadData();
showSuccess(this, this.$t('falukant.house.servants.actions.tidySuccess'));
} catch (err) {
console.error('Error tidying household', err);
showError(this, this.$t('falukant.house.servants.actions.tidyError'));
}
},
handleDaemonMessage(evt) {
try {
const msg = JSON.parse(evt.data);
@@ -344,6 +369,17 @@ export default {
display: flex;
flex-direction: column;
gap: 20px;
/* AppContent gibt dem letzten Kind flex:1 + min-height:0 — sonst schrumpfen
Spalten-Kinder und überlagern sich (z. B. „Dienerschaft“ unter „Kaufe ein Haus“). */
flex: 0 0 auto;
min-height: min-content;
width: 100%;
}
.existing-house,
.servants-panel,
.buyable-houses {
flex-shrink: 0;
}
h2 {
@@ -442,6 +478,34 @@ h2 {
color: var(--color-text-secondary);
}
.servants-reasons {
display: grid;
gap: 8px;
margin-top: 16px;
}
.servants-reasons__label {
color: var(--color-text-secondary);
font-size: 0.88rem;
}
.servants-reasons__list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.servants-reasons__badge {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 999px;
background: rgba(188, 84, 61, 0.12);
color: #9a3c26;
font-size: 0.8rem;
font-weight: 700;
}
.buyable-houses {
display: flex;
flex-direction: column;

View File

@@ -24,6 +24,11 @@
</div>
<section v-if="falukantUser?.character" class="falukant-summary-grid">
<article class="summary-card surface-card">
<span class="summary-card__label">{{ $t('falukant.overview.metadata.certificate') }}</span>
<strong>{{ falukantUser?.certificate ?? '---' }}</strong>
<p>Bestimmt, welche Produktkategorien du derzeit herstellen darfst.</p>
</article>
<article class="summary-card surface-card">
<span class="summary-card__label">Niederlassungen</span>
<strong>{{ branchCount }}</strong>
@@ -109,6 +114,10 @@
<span>{{ $t('falukant.overview.metadata.mainbranch') }}</span>
<strong>{{ falukantUser?.mainBranchRegion?.name }}</strong>
</div>
<div class="detail-list__item">
<span>{{ $t('falukant.overview.metadata.certificate') }}</span>
<strong>{{ falukantUser?.certificate ?? '---' }}</strong>
</div>
</div>
</section>
<section class="overview-panel surface-card">
@@ -347,6 +356,7 @@ export default {
this.socket.off("falukantUserUpdated", this.fetchFalukantUser);
this.socket.off("falukantUpdateStatus");
this.socket.off("falukantUpdateFamily");
this.socket.off("falukantUpdateProductionCertificate");
this.socket.off("children_update");
this.socket.off("falukantBranchUpdate");
this.socket.off("stock_change");
@@ -362,6 +372,9 @@ export default {
this.socket.on("falukantUpdateFamily", (data) => {
this.handleEvent({ event: 'falukantUpdateFamily', ...data });
});
this.socket.on("falukantUpdateProductionCertificate", (data) => {
this.handleEvent({ event: 'falukantUpdateProductionCertificate', ...data });
});
this.socket.on("children_update", (data) => {
this.handleEvent({ event: 'children_update', ...data });
});
@@ -428,6 +441,7 @@ export default {
switch (eventData.event) {
case 'falukantUpdateStatus':
case 'falukantUpdateFamily':
case 'falukantUpdateProductionCertificate':
case 'children_update':
case 'falukantBranchUpdate':
this.queueOverviewRefresh();

View File

@@ -57,7 +57,6 @@
<h2>{{ $t('home.nologin.login.submit') }}</h2>
<p class="login-panel__hint">Mit bestehendem Konto direkt einloggen oder alternativ ohne Konto den Random-Chat testen.</p>
<div class="quick-access-actions">
<button type="button" class="primary-action" @click="doLogin">{{ $t('home.nologin.login.submit') }}</button>
<button type="button" class="secondary-action" @click="openRegisterDialog">{{ $t('home.nologin.login.register') }}</button>
<button type="button" class="secondary-action" @click="openRandomChat">{{ $t('home.nologin.startrandomchat') }}</button>
</div>
@@ -69,9 +68,12 @@
:title="$t('home.nologin.login.passworddescription')" @keydown.enter="doLogin"
ref="passwordInput">
</div>
<div class="login-submit-row">
<button type="button" class="primary-action" @click="doLogin">{{ $t('home.nologin.login.submit') }}</button>
</div>
<div class="stay-logged-in-row">
<label class="stay-logged-in-label">
<input class="stay-logged-in-checkbox" type="checkbox">
<input v-model="rememberMe" class="stay-logged-in-checkbox" type="checkbox">
<span>{{ $t('home.nologin.login.stayLoggedIn') }}</span>
</label>
</div>
@@ -112,6 +114,7 @@ export default {
return {
username: '',
password: '',
rememberMe: true,
};
},
components: {
@@ -140,7 +143,7 @@ export default {
async doLogin() {
try {
const response = await apiClient.post('/api/auth/login', { username: this.username, password: this.password });
this.login(response.data);
this.login({ user: response.data, rememberMe: this.rememberMe });
} catch (error) {
const errorKey = error?.response?.data?.error || 'network';
this.$root.$refs.errorDialog.open(`tr:error.${errorKey}`);
@@ -326,6 +329,11 @@ export default {
gap: 0.65rem;
}
.login-submit-row {
display: flex;
justify-content: flex-start;
}
.primary-action,
.secondary-action {
align-self: flex-start;

View File

@@ -3131,7 +3131,7 @@ export default {
console.log('User username:', this.$store?.state?.user?.username);
const highscoreData = {
userId: this.$store?.state?.user?.id || localStorage.getItem('userid') || 'guest',
userId: this.$store?.state?.user?.id || localStorage.getItem('userid') || sessionStorage.getItem('userid') || 'guest',
nickname: this.$store?.state?.user?.nickname || this.$store?.state?.user?.name || this.$store?.state?.user?.username || 'Gast',
passengersDelivered: this.passengersDelivered,
playtime: playTime,

View File

@@ -205,7 +205,7 @@ export default {
}
},
async fetchImage(image) {
const userId = localStorage.getItem('userid');
const userId = localStorage.getItem('userid') || sessionStorage.getItem('userid');
try {
const response = await apiClient.get(`/api/socialnetwork/image/${image.hash}`, {
headers: {