Files
yourpart3/frontend/src/views/falukant/FamilyView.vue

1173 lines
44 KiB
Vue

<template>
<div class="family-view">
<StatusBar />
<div class="family-layout">
<div class="family-content">
<section class="family-hero surface-card">
<div>
<span class="family-kicker">Familie</span>
<h2>{{ $t('falukant.family.title') }}</h2>
<p>Beziehungen, Kinder und familiäre Entwicklung in einer eigenen Spielweltansicht.</p>
</div>
</section>
<div class="spouse-section">
<h3>{{ $t('falukant.family.spouse.title') }}</h3>
<div v-if="relationships.length > 0" class="relationship-container">
<div class="relationship-row">
<div class="relationship">
<table>
<tr>
<td>{{ $t('falukant.family.relationships.name') }}</td>
<td>
{{ $t('falukant.titles.' + relationships[0].character2.gender + '.' +
relationships[0].character2.nobleTitle) }}
{{ relationships[0].character2.firstName }}
</td>
</tr>
<tr>
<td>{{ $t('falukant.family.spouse.age') }}</td>
<td>{{ relationships[0].character2.age }}</td>
</tr>
<tr>
<td>{{ $t('falukant.family.spouse.mood') }}</td>
<td>{{ relationships[0].character2.mood?.tr ? $t(`falukant.mood.${relationships[0].character2.mood.tr}`) : '—' }}</td>
</tr>
<tr>
<td>{{ $t('falukant.family.spouse.status') }}</td>
<td>{{ $t('falukant.family.statuses.' + relationships[0].relationshipType) }}</td>
</tr>
<tr v-if="relationships[0].marriageSatisfaction != null">
<td>{{ $t('falukant.family.spouse.marriageSatisfaction') }}</td>
<td>
{{ relationships[0].marriageSatisfaction }}
<span class="inline-status-pill" :class="`inline-status-pill--${relationships[0].marriageState || 'stable'}`">
{{ $t('falukant.family.marriageState.' + (relationships[0].marriageState || 'stable')) }}
</span>
</td>
</tr>
<tr v-if="relationships[0].relationshipType === 'wooing'">
<td>{{ $t('falukant.family.spouse.progress') }}</td>
<td>
<div class="progress">
<div class="progress-inner" :style="{
width: normalizeWooingProgress(relationships[0].progress) + '%',
backgroundColor: progressColor(relationships[0].progress)
}"></div>
</div>
</td>
</tr>
<tr v-if="relationships[0].relationshipType === 'engaged'" colspan="2">
<button @click="jumpToPartyForm">{{ $t('falukant.family.spouse.jumpToPartyForm')
}}</button>
</tr>
</table>
<ul>
<li v-for="trait in relationships[0].character2.traits" :key="trait.id">
{{ $t(`falukant.character.${trait.tr}`) }}
</li>
</ul>
</div>
<div class="partner-character-3d">
<Character3D
:gender="relationships[0].character2.gender"
:age="relationships[0].character2.age"
/>
</div>
</div>
<div v-if="relationships[0].relationshipType === 'wooing'" class="gifts-section">
<h3>{{ $t('falukant.family.spouse.wooing.gifts') }}</h3>
<table class="spouse-table">
<thead>
<tr>
<th></th>
<th>{{ $t('falukant.family.spouse.wooing.gift') }}</th>
<th>{{ $t('falukant.family.spouse.wooing.effect') }}</th>
<th>{{ $t('falukant.family.spouse.wooing.value') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="gift in gifts" :key="gift.id">
<td>
<input type="radio" name="gift" :value="gift.id" v-model="selectedGiftId" />
</td>
<td>{{ $t(`falukant.gifts.${gift.name}`) }}</td>
<td>{{ getEffect(gift) }}</td>
<td>{{ formatCost(gift.cost) }}</td>
</tr>
</tbody>
</table>
<div class="wooing-actions">
<button @click="sendGift" class="button">{{ $t('falukant.family.spouse.wooing.sendGift') }}</button>
<button @click="cancelWooing" class="button button-secondary">{{ $t('falukant.family.spouse.wooing.cancel') }}</button>
</div>
</div>
</div>
<div v-else-if="proposals && proposals.length > 0">
<table class="spouse-table">
<thead>
<tr>
<th>{{ $t('falukant.family.spouse.select') }}</th>
<th>{{ $t('falukant.family.spouse.name') }}</th>
<th>{{ $t('falukant.family.spouse.age') }}</th>
<th>{{ $t('falukant.family.spouse.marriagecost') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="proposal in proposals" :key="proposal.id">
<td><input type="radio" name="spouse" :value="proposal.proposedCharacterId"
v-model="selectedProposalId"></td>
<td>{{
$t(`falukant.titles.${proposal.proposedCharacterGender}.${proposal.proposedCharacterNobleTitle}`)
}} {{ proposal.proposedCharacterName }}</td>
<td>{{ proposal.proposedCharacterAge }}</td>
<td>{{ formatCost(proposal.cost) }}</td>
</tr>
</tbody>
</table>
<div>{{ $t('falukant.family.spouse.notice') }}</div>
<div v-if="selectedProposalId">
<button @click="acceptProposal">{{ $t('falukant.family.spouse.accept') }}</button>
</div>
</div>
</div>
<section v-if="marriageSatisfaction != null || householdTension" class="marriage-overview surface-card">
<div class="marriage-overview__item" v-if="marriageSatisfaction != null">
<span class="marriage-overview__label">{{ $t('falukant.family.spouse.marriageSatisfaction') }}</span>
<strong>{{ marriageSatisfaction }}</strong>
</div>
<div class="marriage-overview__item" v-if="marriageState">
<span class="marriage-overview__label">{{ $t('falukant.family.spouse.marriageState') }}</span>
<span class="inline-status-pill" :class="`inline-status-pill--${marriageState}`">
{{ $t('falukant.family.marriageState.' + marriageState) }}
</span>
</div>
<div class="marriage-overview__item" v-if="householdTension">
<span class="marriage-overview__label">{{ $t('falukant.family.householdTension.label') }}</span>
<span class="inline-status-pill" :class="`inline-status-pill--${householdTension}`">
{{ $t('falukant.family.householdTension.' + householdTension) }}
</span>
</div>
</section>
<div class="children-section">
<h3>{{ $t('falukant.family.children.title') }}</h3>
<div v-if="children && children.length > 0" class="children-container">
<!-- 3D-Modell für ausgewähltes Kind rechts neben Liste -->
<div class="children-list">
<table>
<thead>
<tr>
<th>{{ $t('falukant.family.children.name') }}</th>
<th>{{ $t('falukant.family.children.age') }}</th>
<th>{{ $t('falukant.family.children.heir') }}</th>
<th>{{ $t('falukant.family.children.actions') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(child, index) in children" :key="index">
<td v-if="child.hasName">
{{ child.name }}
<span v-if="child.legitimacy && child.legitimacy !== 'legitimate'" class="child-origin-badge">
{{ $t('falukant.family.children.legitimacy.' + child.legitimacy) }}
</span>
</td>
<td v-else>
<button @click="jumpToChurchForm">{{ $t('falukant.family.children.baptism')
}}</button>
</td>
<td>{{ child.age }}</td>
<td>
<span v-if="child.isHeir" class="heir-badge">{{ $t('falukant.family.children.isHeir') }}</span>
<button v-else-if="child.hasName" @click="setAsHeir(child)" class="set-heir-button">
{{ $t('falukant.family.children.setAsHeir') }}
</button>
</td>
<td>
<button @click="showChildDetails(child)">
{{ $t('falukant.family.children.detailButton') }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="child-character-3d" v-if="selectedChild">
<Character3D
:gender="selectedChild.gender"
:age="selectedChild.age"
/>
</div>
</div>
<div v-else>
<p>{{ $t('falukant.family.children.none') }}</p>
</div>
</div>
<!-- Liebhaber / Geliebte -->
<div class="lovers-section">
<h3>{{ $t('falukant.family.lovers.title') }}</h3>
<div v-if="lovers && lovers.length > 0" class="lovers-grid">
<article v-for="lover in lovers" :key="lover.relationshipId" class="lover-card surface-card">
<div class="lover-card__header">
<div>
<strong>{{ $t('falukant.titles.' + lover.gender + '.' + lover.title) }} {{ lover.name }}</strong>
<div class="lover-card__role">
{{ $t('falukant.family.lovers.role.' + (lover.role || 'lover')) }}
</div>
</div>
<span class="inline-status-pill" :class="`inline-status-pill--${lover.riskState || 'low'}`">
{{ $t('falukant.family.lovers.risk.' + (lover.riskState || 'low')) }}
</span>
</div>
<dl class="lover-card__stats">
<div>
<dt>{{ $t('falukant.family.lovers.affection') }}</dt>
<dd>{{ lover.affection }}</dd>
</div>
<div>
<dt>{{ $t('falukant.family.lovers.visibility') }}</dt>
<dd>{{ lover.visibility }}</dd>
</div>
<div>
<dt>{{ $t('falukant.family.lovers.discretion') }}</dt>
<dd>{{ lover.discretion }}</dd>
</div>
<div>
<dt>{{ $t('falukant.family.lovers.maintenance') }}</dt>
<dd>{{ lover.maintenanceLevel }}</dd>
</div>
<div>
<dt>{{ $t('falukant.family.lovers.monthlyCost') }}</dt>
<dd>{{ formatCost(lover.monthlyCost || 0) }}</dd>
</div>
<div>
<dt>{{ $t('falukant.family.lovers.statusFit') }}</dt>
<dd>{{ lover.statusFit }}</dd>
</div>
</dl>
<div class="lover-card__meta">
<span v-if="lover.acknowledged" class="lover-meta-badge">
{{ $t('falukant.family.lovers.acknowledged') }}
</span>
<span v-if="lover.monthsUnderfunded > 0" class="lover-meta-badge lover-meta-badge--warning">
{{ $t('falukant.family.lovers.underfunded', { count: lover.monthsUnderfunded }) }}
</span>
</div>
<div class="lover-card__actions">
<button class="button button--secondary" @click="setLoverMaintenance(lover, 25)">
{{ $t('falukant.family.lovers.actions.maintenanceLow') }}
</button>
<button class="button button--secondary" @click="setLoverMaintenance(lover, 50)">
{{ $t('falukant.family.lovers.actions.maintenanceMedium') }}
</button>
<button class="button button--secondary" @click="setLoverMaintenance(lover, 75)">
{{ $t('falukant.family.lovers.actions.maintenanceHigh') }}
</button>
<button
v-if="!lover.acknowledged"
class="button button--secondary"
@click="acknowledgeLover(lover)"
>
{{ $t('falukant.family.lovers.actions.acknowledge') }}
</button>
<button class="button button--danger" @click="endLoverRelationship(lover)">
{{ $t('falukant.family.lovers.actions.end') }}
</button>
</div>
</article>
</div>
<div v-else>
<p>{{ $t('falukant.family.lovers.none') }}</p>
</div>
<div class="lover-candidates surface-card">
<h4>{{ $t('falukant.family.lovers.candidates.title') }}</h4>
<div v-if="possibleLovers && possibleLovers.length > 0" class="lover-candidates__grid">
<article v-for="candidate in possibleLovers" :key="candidate.characterId" class="lover-candidate-card">
<div class="lover-candidate-card__main">
<strong>{{ $t('falukant.titles.' + candidate.gender + '.' + candidate.title) }} {{ candidate.name }}</strong>
<span>{{ $t('falukant.family.spouse.age') }}: {{ candidate.age }}</span>
<span>{{ $t('falukant.family.lovers.statusFit') }}: {{ candidate.statusFit }}</span>
<span>{{ $t('falukant.family.lovers.monthlyCost') }}: {{ formatCost(candidate.estimatedMonthlyCost || 0) }}</span>
</div>
<div class="lover-candidate-card__actions">
<label class="lover-candidate-card__label">
{{ $t('falukant.family.lovers.candidates.roleLabel') }}
</label>
<select class="lover-candidate-card__select" v-model="candidateRoles[candidate.characterId]">
<option value="secret_affair">{{ $t('falukant.family.lovers.role.secret_affair') }}</option>
<option value="lover">{{ $t('falukant.family.lovers.role.lover') }}</option>
<option value="mistress_or_favorite">{{ $t('falukant.family.lovers.role.mistress_or_favorite') }}</option>
</select>
<button class="button button--secondary" @click="createLoverRelationship(candidate)">
{{ $t('falukant.family.lovers.actions.start') }}
</button>
</div>
</article>
</div>
<p v-else>{{ $t('falukant.family.lovers.candidates.none') }}</p>
</div>
</div>
</div>
</div>
<ChildDetailsDialog ref="childDetailsDialog" />
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue'
import ChildDetailsDialog from '@/dialogues/falukant/ChildDetailsDialog.vue'
import Character3D from '@/components/Character3D.vue'
import apiClient from '@/utils/axios.js'
import { confirmAction, showError, showInfo, showSuccess } from '@/utils/feedback.js'
import { mapState } from 'vuex'
const WOOING_PROGRESS_TARGET = 70
export default {
name: 'FamilyView',
components: {
StatusBar,
ChildDetailsDialog,
Character3D
},
data() {
return {
relationships: [],
children: [],
lovers: [],
possibleLovers: [],
candidateRoles: {},
deathPartners: [],
proposals: [],
selectedProposalId: null,
gifts: [],
selectedGiftId: null,
moodAffects: [],
characterAffects: [],
ownCharacter: null,
marriageSatisfaction: null,
marriageState: null,
householdTension: null,
selectedChild: null,
pendingFamilyRefresh: null
}
},
computed: {
...mapState(['socket', 'daemonSocket', 'user'])
},
watch: {
socket(newVal, oldVal) {
if (oldVal) this.teardownSocketEvents();
if (newVal) this.setupSocketEvents();
},
daemonSocket(newVal, oldVal) {
if (oldVal) this.teardownDaemonListeners();
if (newVal) this.setupDaemonListeners();
}
},
async mounted() {
await this.loadOwnCharacter();
await this.loadFamilyData();
await this.loadGifts();
await this.loadMoodAffects();
await this.loadCharacterAffects();
this.setupSocketEvents();
this.setupDaemonListeners();
},
beforeUnmount() {
this.teardownSocketEvents();
this.teardownDaemonListeners();
if (this.pendingFamilyRefresh) {
clearTimeout(this.pendingFamilyRefresh);
this.pendingFamilyRefresh = null;
}
},
methods: {
setupSocketEvents() {
this.teardownSocketEvents();
if (this.socket) {
this._falukantUpdateStatusHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data });
this._falukantUpdateFamilyHandler = (data) => this.handleEvent({ event: 'falukantUpdateFamily', ...data });
this._childrenUpdateHandler = (data) => this.handleEvent({ event: 'children_update', ...data });
this._familyChangedHandler = (data) => this.handleEvent({ event: 'familychanged', ...data });
this.socket.on('falukantUpdateStatus', this._falukantUpdateStatusHandler);
this.socket.on('falukantUpdateFamily', this._falukantUpdateFamilyHandler);
this.socket.on('children_update', this._childrenUpdateHandler);
this.socket.on('familychanged', this._familyChangedHandler);
} else {
setTimeout(() => this.setupSocketEvents(), 1000);
}
},
teardownSocketEvents() {
if (!this.socket) return;
if (this._falukantUpdateStatusHandler) this.socket.off('falukantUpdateStatus', this._falukantUpdateStatusHandler);
if (this._falukantUpdateFamilyHandler) this.socket.off('falukantUpdateFamily', this._falukantUpdateFamilyHandler);
if (this._childrenUpdateHandler) this.socket.off('children_update', this._childrenUpdateHandler);
if (this._familyChangedHandler) this.socket.off('familychanged', this._familyChangedHandler);
},
setupDaemonListeners() {
this.teardownDaemonListeners();
if (!this.daemonSocket) return;
this._daemonFamilyHandler = (event) => {
if (event.data === 'ping') return;
try {
const message = JSON.parse(event.data);
if ([
'falukantUpdateStatus',
'falukantUpdateFamily',
'children_update',
'familychanged',
'falukant_family_scandal_hint'
].includes(message.event)) {
this.handleEvent(message);
}
} catch (_) {}
};
this.daemonSocket.addEventListener('message', this._daemonFamilyHandler);
},
teardownDaemonListeners() {
if (this.daemonSocket && this._daemonFamilyHandler) {
this.daemonSocket.removeEventListener('message', this._daemonFamilyHandler);
this._daemonFamilyHandler = null;
}
},
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));
},
queueFamilyRefresh({ reloadCharacter = false } = {}) {
if (this.pendingFamilyRefresh) {
clearTimeout(this.pendingFamilyRefresh);
}
this.pendingFamilyRefresh = setTimeout(async () => {
this.pendingFamilyRefresh = null;
await this.loadFamilyData();
if (reloadCharacter) {
await this.loadOwnCharacter();
}
}, 120);
},
handleEvent(eventData) {
if (!this.matchesCurrentUser(eventData)) {
return;
}
switch (eventData.event) {
case 'falukantUpdateStatus':
case 'familychanged':
this.queueFamilyRefresh({ reloadCharacter: true });
break;
case 'children_update':
this.queueFamilyRefresh({ reloadCharacter: false });
break;
case 'falukantUpdateFamily':
if (eventData.reason === 'scandal') {
showInfo(this, this.$t('falukant.family.notifications.scandal'));
} else if (eventData.reason === 'lover_birth') {
showInfo(this, this.$t('falukant.family.notifications.loverBirth'));
}
this.queueFamilyRefresh({ reloadCharacter: eventData.reason === 'monthly' || eventData.reason === 'daily' });
break;
}
},
async loadFamilyData() {
try {
const response = await apiClient.get('/api/falukant/family');
this.relationships = response.data.relationships;
this.children = response.data.children;
this.lovers = response.data.lovers;
this.possibleLovers = response.data.possibleLovers || [];
this.syncCandidateRoles();
this.proposals = response.data.possiblePartners;
this.deathPartners = response.data.deathPartners;
this.marriageSatisfaction = response.data.marriageSatisfaction;
this.marriageState = response.data.marriageState;
this.householdTension = response.data.householdTension;
} catch (error) {
console.error('Error loading family data:', error);
}
},
async loadOwnCharacter() {
try {
const response = await apiClient.get('/api/falukant/user');
this.ownCharacter = response.data.character;
} catch (error) {
console.error('Error loading own character:', error);
}
},
showChildDetails(child) {
this.selectedChild = child;
this.$refs.childDetailsDialog?.open(child);
},
async setAsHeir(child) {
if (!child.childCharacterId) {
showError(this, 'tr:falukant.family.children.heirSetError');
return;
}
try {
await apiClient.post('/api/falukant/family/set-heir', {
childCharacterId: child.childCharacterId
});
await this.loadFamilyData();
showSuccess(this, 'tr:falukant.family.children.heirSetSuccess');
} catch (error) {
console.error('Error setting heir:', error);
showError(this, 'tr:falukant.family.children.heirSetError');
}
},
async setLoverMaintenance(lover, maintenanceLevel) {
try {
await apiClient.post(`/api/falukant/family/lover/${lover.relationshipId}/maintenance`, {
maintenanceLevel
});
await this.loadFamilyData();
showSuccess(this, this.$t('falukant.family.lovers.actions.maintenanceSuccess'));
} catch (error) {
console.error('Error updating lover maintenance:', error);
showError(this, this.$t('falukant.family.lovers.actions.maintenanceError'));
}
},
async acknowledgeLover(lover) {
try {
await apiClient.post(`/api/falukant/family/lover/${lover.relationshipId}/acknowledge`);
await this.loadFamilyData();
showSuccess(this, this.$t('falukant.family.lovers.actions.acknowledgeSuccess'));
} catch (error) {
console.error('Error acknowledging lover:', error);
showError(this, this.$t('falukant.family.lovers.actions.acknowledgeError'));
}
},
async endLoverRelationship(lover) {
const confirmed = await confirmAction(this, {
title: this.$t('falukant.family.lovers.actions.end'),
message: this.$t('falukant.family.lovers.actions.endConfirm')
});
if (!confirmed) return;
try {
await apiClient.post(`/api/falukant/family/lover/${lover.relationshipId}/end`);
await this.loadFamilyData();
showSuccess(this, this.$t('falukant.family.lovers.actions.endSuccess'));
} catch (error) {
console.error('Error ending lover relationship:', error);
showError(this, this.$t('falukant.family.lovers.actions.endError'));
}
},
async createLoverRelationship(candidate) {
try {
await apiClient.post('/api/falukant/family/lover', {
targetCharacterId: candidate.characterId,
loverRole: this.getCandidateRole(candidate.characterId)
});
await this.loadFamilyData();
showSuccess(this, this.$t('falukant.family.lovers.actions.startSuccess'));
} catch (error) {
console.error('Error creating lover relationship:', error);
showError(this, this.$t('falukant.family.lovers.actions.startError'));
}
},
syncCandidateRoles() {
const nextRoles = {};
for (const candidate of this.possibleLovers) {
nextRoles[candidate.characterId] = this.candidateRoles[candidate.characterId] || 'secret_affair';
}
this.candidateRoles = nextRoles;
},
getCandidateRole(characterId) {
return this.candidateRoles[characterId] || 'secret_affair';
},
formatCost(value) {
return new Intl.NumberFormat(navigator.language, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);
},
getEffect(gift) {
const relationship = this.relationships[0];
if (!relationship || !relationship.character2) {
return 0;
}
const partner = relationship.character2;
const currentMoodId = partner.moodId;
const moodEntry = gift.moodsAffects.find(ma => ma.mood_id === currentMoodId);
const moodValue = moodEntry ? moodEntry.suitability : 0;
let highestCharacterValue = 0;
// traits ist ein Array von Trait-Objekten mit id und tr
for (const trait of partner.traits || []) {
const charEntry = gift.charactersAffects.find(ca => ca.trait_id === trait.id);
if (charEntry && charEntry.suitability > highestCharacterValue) {
highestCharacterValue = charEntry.suitability;
}
}
return Math.round((moodValue + highestCharacterValue) / 2);
},
async acceptProposal() {
const response = await apiClient.post('/api/falukant/family/acceptmarriageproposal'
, { proposalId: this.selectedProposalId });
this.loadFamilyData();
},
async loadGifts() {
try {
const response = await apiClient.get('/api/falukant/family/gifts');
this.gifts = response.data || [];
} catch (error) {
console.error('Error loading gifts:', error);
this.gifts = []; // Leeres Array bei Fehler
}
},
async cancelWooing() {
const confirmed = await confirmAction(this, {
title: 'Werbung abbrechen',
message: this.$t('falukant.family.spouse.wooing.cancelConfirm')
});
if (!confirmed) return;
try {
await apiClient.post('/api/falukant/family/cancel-wooing');
await this.loadFamilyData();
this.$root.$refs.messageDialog?.open('tr:falukant.family.spouse.wooing.cancelSuccess');
} catch (error) {
console.error('Error cancelling wooing:', error);
if (error?.response?.status === 412) {
const retryAtIso = error.response?.data?.retryAt;
const retryStr = retryAtIso ? new Date(retryAtIso).toLocaleString(navigator.language, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '';
const msg = this.$t('falukant.family.spouse.wooing.cancelTooSoon');
this.$root.$refs.errorDialog?.open(retryStr ? `${msg}${this.$t('falukant.family.sendgift.nextGiftAt')}: ${retryStr}` : msg);
} else {
this.$root.$refs.errorDialog?.open('tr:falukant.family.spouse.wooing.cancelError');
}
}
},
async sendGift() {
if (!this.selectedGiftId) {
this.$root.$refs.errorDialog.open(`tr:falukant.family.sendgift.error.nogiftselected`);
return;
}
try {
await apiClient.post('/api/falukant/family/gift'
, { giftId: this.selectedGiftId });
this.loadFamilyData();
this.$root.$refs.messageDialog.open('tr:falukant.family.sendgift.success');
} catch (error) {
console.log(error.response);
if (error.response?.status === 412) {
const retryAtIso = error.response?.data?.retryAt;
const code = error.response?.data?.error || error.response?.data?.message;
if (retryAtIso) {
const retryStr = new Date(retryAtIso).toLocaleString(navigator.language, {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit'
});
const baseMsg = this.$t(`falukant.family.sendgift.error.${code}`);
this.$root.$refs.errorDialog.open(`${baseMsg}${this.$t('falukant.family.sendgift.nextGiftAt')}: ${retryStr}`);
} else {
this.$root.$refs.errorDialog.open(`tr:falukant.family.sendgift.error.${code}`);
}
} else {
this.$root.$refs.errorDialog.open(`tr:falukant.family.sendgift.error.generic`);
}
}
},
async loadMoodAffects() {
try {
const response = await apiClient.get('/api/falukant/mood/affect');
this.moodAffects = response.data;
} catch (error) {
console.error(error);
}
},
async loadCharacterAffects() {
try {
const response = await apiClient.get('/api/falukant/character/affect');
this.characterAffects = response.data;
} catch (error) {
console.error(error);
}
},
progressColor(p) {
const pct = this.normalizeWooingProgress(p) / 100;
const red = Math.round(255 * (1 - pct));
const green = Math.round(255 * pct);
return `rgb(${red}, ${green}, 0)`;
},
normalizeWooingProgress(p) {
const raw = Number(p) || 0
const normalized = (raw / WOOING_PROGRESS_TARGET) * 100
return Math.max(0, Math.min(100, normalized))
},
jumpToPartyForm() {
this.$router.push({
name: 'ReputationView',
query: { tab: 'party' }
});
},
jumpToChurchForm() {
this.$router.push({
name: 'ChurchView',
});
},
getEffect(gift) {
// aktueller Partner
const partner = this.relationships[0].character2;
// seine aktuelle Mood-ID
const moodId = partner.mood?.id ?? partner.mood_id;
// 1) Mood-Eintrag finden
const moodEntry = gift.moodsAffects.find(ma => ma.mood_id === moodId);
const moodValue = moodEntry ? moodEntry.suitability : 0;
// 2) Trait-Einträge matchen
let highestTraitValue = 0;
for (const trait of partner.traits) {
const charEntry = gift.charactersAffects.find(ca => ca.trait_id === trait.id);
if (charEntry && charEntry.suitability > highestTraitValue) {
highestTraitValue = charEntry.suitability;
}
}
// Durchschnitt, gerundet
return Math.round((moodValue + highestTraitValue) / 2);
},
}
}
</script>
<style scoped lang="scss">
.family-layout {
display: flex;
gap: 20px;
align-items: flex-start;
padding-top: 0;
}
.family-hero {
padding: 24px 26px;
margin-bottom: 16px;
background:
radial-gradient(circle at top right, rgba(248, 162, 43, 0.16), transparent 28%),
linear-gradient(180deg, rgba(255, 250, 243, 0.98) 0%, rgba(247, 238, 224, 0.98) 100%);
}
.family-kicker {
display: inline-block;
margin-bottom: 10px;
padding: 4px 10px;
border-radius: 999px;
background: rgba(248, 162, 43, 0.14);
color: #8a5411;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.family-hero p {
margin: 0;
color: var(--color-text-secondary);
}
.marriage-overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
margin-bottom: 18px;
padding: 16px 18px;
}
.marriage-overview__item {
display: flex;
flex-direction: column;
gap: 6px;
}
.marriage-overview__label {
color: var(--color-text-secondary);
font-size: 0.88rem;
}
.inline-status-pill {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 700;
width: fit-content;
}
.inline-status-pill--stable,
.inline-status-pill--low {
background: rgba(66, 140, 87, 0.16);
color: #2e6b42;
}
.inline-status-pill--strained,
.inline-status-pill--medium {
background: rgba(230, 172, 52, 0.18);
color: #875e08;
}
.inline-status-pill--crisis,
.inline-status-pill--high {
background: rgba(188, 84, 61, 0.16);
color: #9a3c26;
}
.child-origin-badge {
margin-left: 8px;
padding: 2px 8px;
border-radius: 999px;
background: rgba(131, 104, 73, 0.14);
color: #6a4d2f;
font-size: 0.72rem;
font-weight: 700;
}
.lovers-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 14px;
}
.lover-card {
padding: 16px 18px;
}
.lover-card__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
margin-bottom: 12px;
}
.lover-card__role {
margin-top: 4px;
color: var(--color-text-secondary);
font-size: 0.88rem;
}
.lover-card__stats {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 16px;
margin: 0;
}
.lover-card__stats div {
display: flex;
flex-direction: column;
gap: 3px;
}
.lover-card__stats dt {
color: var(--color-text-secondary);
font-size: 0.78rem;
}
.lover-card__stats dd {
margin: 0;
font-weight: 700;
}
.lover-card__meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.lover-card__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.lover-card__actions .button {
min-width: 0;
}
.lover-candidates {
margin-top: 18px;
padding: 16px 18px;
}
.lover-candidates__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 12px;
margin-top: 12px;
}
.lover-candidate-card {
display: flex;
justify-content: space-between;
align-items: center;
gap: 14px;
padding: 14px 16px;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: rgba(255, 250, 243, 0.88);
}
.lover-candidate-card__main {
display: flex;
flex-direction: column;
gap: 4px;
}
.lover-candidate-card__main span {
color: var(--color-text-secondary);
font-size: 0.88rem;
}
.lover-candidate-card__actions {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 180px;
}
.lover-candidate-card__label {
color: var(--color-text-secondary);
font-size: 0.8rem;
font-weight: 600;
}
.lover-candidate-card__select {
width: 100%;
min-height: 40px;
}
.lover-meta-badge {
display: inline-flex;
align-items: center;
padding: 3px 9px;
border-radius: 999px;
background: rgba(66, 140, 87, 0.14);
color: #2e6b42;
font-size: 0.76rem;
font-weight: 700;
}
.lover-meta-badge--warning {
background: rgba(188, 84, 61, 0.14);
color: #9a3c26;
}
.self-character-3d {
width: 250px;
height: 350px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #fdf1db;
flex-shrink: 0;
}
.family-content {
flex: 1;
max-width: var(--content-max-width);
margin: 0 auto;
padding-bottom: 24px;
}
.spouse-section,
.children-section,
.lovers-section {
border: 1px solid var(--color-border);
margin: 12px 0;
border-radius: var(--radius-lg);
padding: 16px;
background: rgba(255, 252, 247, 0.86);
box-shadow: var(--shadow-soft);
}
.relationship-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.relationship-row {
display: flex;
gap: 20px;
align-items: flex-start;
}
.relationship {
flex: 0 1 auto;
}
.partner-character-3d {
width: 200px;
height: 280px;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background-color: #fdf1db;
flex-shrink: 0;
}
.gifts-section {
width: 100%;
margin-top: 10px;
}
.children-container {
display: flex;
gap: 20px;
align-items: flex-start;
}
.children-list {
flex: 1;
}
.child-character-3d {
width: 200px;
height: 280px;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background-color: #fdf1db;
flex-shrink: 0;
}
.spouse-section table,
.children-section table {
margin-top: 10px;
border-collapse: collapse;
}
.spouse-section th,
.spouse-section td,
.children-section th,
.children-section td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.spouse-section th {
background-color: #f2f2f2;
}
.children-section th {
background-color: #f9f9f9;
}
.lovers-section {
ul {
list-style: none;
padding-left: 0;
}
li {
padding: 4px 0;
}
}
.spouse-table th,
.spouse-table td {
white-space: nowrap;
}
.spouse-table th:first-child,
.spouse-table td:first-child {
width: 20px;
}
.spouse-table th:nth-child(3),
.spouse-table td:nth-child(3) {
width: 30px;
}
.spouse-table th:nth-child(4),
.spouse-table td:nth-child(4) {
width: 50px;
}
.relationship>table,
.relationship>ul {
display: inline-block;
margin-right: 1em;
vertical-align: top;
}
.relationship>ul {
list-style: none;
}
.progress {
width: 100%;
background-color: #e5e7eb;
border-radius: 0.25rem;
overflow: hidden;
height: 1rem;
}
.progress-inner {
height: 100%;
transition: width 0.3s ease, background-color 0.3s ease;
}
.heir-badge {
display: inline-block;
padding: 4px 8px;
background-color: #4CAF50;
color: white;
border-radius: 4px;
font-size: 0.9em;
font-weight: bold;
}
.set-heir-button {
padding: 4px 8px;
background-color: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.set-heir-button:hover {
background-color: #218838;
}
@media (max-width: 960px) {
.relationship-row,
.children-container {
flex-direction: column;
}
}
</style>