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

@@ -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();