From 2b83c45e972994831141c92ba6ad4acea4749336 Mon Sep 17 00:00:00 2001
From: "Torsten Schulz (local)"
Date: Mon, 30 Mar 2026 14:36:02 +0200
Subject: [PATCH] feat(family): enhance family view and character pregnancy
handling
- Updated the FalukantCharacter model to include a default scope that excludes pregnancy-related fields for compatibility with older databases.
- Implemented a new method in FalukantService to conditionally retrieve pregnancy information based on database schema.
- Enhanced the FamilyView component to display a summary navigation for family relationships, including partners, children, and lovers.
- Updated internationalization files to include new translations for family-related terms and summaries.
---
backend/models/falukant/data/character.js | 7 +-
backend/services/falukantService.js | 27 +-
frontend/src/i18n/locales/de/falukant.json | 15 +
frontend/src/i18n/locales/en/falukant.json | 16 ++
frontend/src/i18n/locales/es/falukant.json | 15 +
frontend/src/views/falukant/FamilyView.vue | 306 ++++++++++++++++-----
6 files changed, 307 insertions(+), 79 deletions(-)
diff --git a/backend/models/falukant/data/character.js b/backend/models/falukant/data/character.js
index c4327f6..74f36e2 100644
--- a/backend/models/falukant/data/character.js
+++ b/backend/models/falukant/data/character.js
@@ -61,7 +61,12 @@ FalukantCharacter.init(
tableName: 'character',
schema: 'falukant_data',
timestamps: true,
- underscored: true}
+ underscored: true,
+ // Spalten erst nach Migration 20260330000000; ohne Exclude würde SELECT/INSERT auf alten DBs fehlschlagen
+ defaultScope: {
+ attributes: { exclude: ['pregnancyDueAt', 'pregnancyFatherCharacterId'] },
+ },
+ }
);
export default FalukantCharacter;
diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js
index 62347ac..ecfde99 100644
--- a/backend/services/falukantService.js
+++ b/backend/services/falukantService.js
@@ -3320,11 +3320,31 @@ class FalukantService extends BaseService {
return { result: 'ok' };
}
+ /** Liest Schwangerschaft nur wenn DB-Spalten existieren (nach Migration). */
+ async _getCharacterPregnancyOptional(characterId) {
+ try {
+ const rows = await sequelize.query(
+ `SELECT pregnancy_due_at, pregnancy_father_character_id
+ FROM falukant_data."character" WHERE id = :id`,
+ { replacements: { id: characterId }, type: Sequelize.QueryTypes.SELECT }
+ );
+ const row = rows[0];
+ if (!row?.pregnancy_due_at) return null;
+ return {
+ dueAt: row.pregnancy_due_at,
+ fatherCharacterId: row.pregnancy_father_character_id,
+ };
+ } catch {
+ return null;
+ }
+ }
+
async getFamily(hashedUserId) {
const user = await this.getFalukantUserByHashedId(hashedUserId);
if (!user) throw new Error('User not found');
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
if (!character) throw new Error('Character not found for this user');
+ const pregnancy = await this._getCharacterPregnancyOptional(character.id);
// Load relationships without includes to avoid EagerLoadingError
const relRows = await Relationship.findAll({
where: { character1Id: character.id },
@@ -3519,12 +3539,7 @@ class FalukantService extends BaseService {
children: children.map(({ _createdAt, ...rest }) => rest),
possiblePartners: [],
possibleLovers: [],
- pregnancy: character.pregnancyDueAt
- ? {
- dueAt: character.pregnancyDueAt,
- fatherCharacterId: character.pregnancyFatherCharacterId,
- }
- : null,
+ pregnancy,
};
const ownAge = calcAge(character.birthdate);
if (ownAge >= 12) {
diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json
index 7c34260..a603a13 100644
--- a/frontend/src/i18n/locales/de/falukant.json
+++ b/frontend/src/i18n/locales/de/falukant.json
@@ -536,6 +536,20 @@
},
"family": {
"title": "Familie",
+ "heroIntro": "Beziehungen, Kinder und Entwicklung — unten nach Bereichen geordnet.",
+ "summary": {
+ "partnerChip": "Partner",
+ "childrenChip": "Kinder",
+ "loversChip": "Liebschaften",
+ "proposalsAvailable": "Verlobung möglich",
+ "noPartner": "Kein Partner"
+ },
+ "tabs": {
+ "partner": "Partner & Ehe",
+ "children": "Kinder",
+ "lovers": "Liebschaften"
+ },
+ "tabsAria": "Familienbereiche",
"debtorsPrison": {
"familyWarning": "Anhaltender Kreditverzug belastet Ehe, Haushalt und Liebschaften.",
"familyImpact": "Der Schuldturm schadet Ehe, Hausfrieden und der Stabilität von Liebschaften."
@@ -546,6 +560,7 @@
},
"spouse": {
"title": "Beziehung",
+ "traitsToggle": "Charaktereigenschaften",
"name": "Name",
"age": "Alter",
"status": "Status",
diff --git a/frontend/src/i18n/locales/en/falukant.json b/frontend/src/i18n/locales/en/falukant.json
index 86a88b2..0425433 100644
--- a/frontend/src/i18n/locales/en/falukant.json
+++ b/frontend/src/i18n/locales/en/falukant.json
@@ -607,6 +607,21 @@
}
},
"family": {
+ "title": "Family",
+ "heroIntro": "Relationships, children and development — organized by section below.",
+ "summary": {
+ "partnerChip": "Partner",
+ "childrenChip": "Children",
+ "loversChip": "Affairs",
+ "proposalsAvailable": "Betrothal available",
+ "noPartner": "No partner"
+ },
+ "tabs": {
+ "partner": "Partner & marriage",
+ "children": "Children",
+ "lovers": "Affairs"
+ },
+ "tabsAria": "Family sections",
"debtorsPrison": {
"familyWarning": "Ongoing debt delinquency puts strain on marriage, household and affairs.",
"familyImpact": "Debtors' prison damages marriage, household peace and the stability of affairs."
@@ -653,6 +668,7 @@
}
},
"spouse": {
+ "traitsToggle": "Character traits",
"marriageSatisfaction": "Marriage Satisfaction",
"marriageState": "Marriage State",
"wooing": {
diff --git a/frontend/src/i18n/locales/es/falukant.json b/frontend/src/i18n/locales/es/falukant.json
index 0618596..f4c8e4a 100644
--- a/frontend/src/i18n/locales/es/falukant.json
+++ b/frontend/src/i18n/locales/es/falukant.json
@@ -517,6 +517,20 @@
},
"family": {
"title": "Familia",
+ "heroIntro": "Relaciones, hijos y desarrollo — ordenado por secciones abajo.",
+ "summary": {
+ "partnerChip": "Pareja",
+ "childrenChip": "Hijos",
+ "loversChip": "Relaciones",
+ "proposalsAvailable": "Compromiso posible",
+ "noPartner": "Sin pareja"
+ },
+ "tabs": {
+ "partner": "Pareja y matrimonio",
+ "children": "Hijos",
+ "lovers": "Relaciones"
+ },
+ "tabsAria": "Secciones de familia",
"debtorsPrison": {
"familyWarning": "La mora continuada perjudica el matrimonio, el hogar y las relaciones.",
"familyImpact": "La prisión por deudas daña el matrimonio, la paz del hogar y la estabilidad de las relaciones."
@@ -527,6 +541,7 @@
},
"spouse": {
"title": "Relación",
+ "traitsToggle": "Rasgos de carácter",
"name": "Nombre",
"age": "Edad",
"status": "Estado",
diff --git a/frontend/src/views/falukant/FamilyView.vue b/frontend/src/views/falukant/FamilyView.vue
index 5975aed..91d2725 100644
--- a/frontend/src/views/falukant/FamilyView.vue
+++ b/frontend/src/views/falukant/FamilyView.vue
@@ -7,7 +7,7 @@
Familie
{{ $t('falukant.family.title') }}
-
Beziehungen, Kinder und familiäre Entwicklung in einer eigenen Spielweltansicht.
+
{{ $t('falukant.family.heroIntro') }}
@@ -36,62 +36,116 @@
+
+
+
{{ $t('falukant.family.spouse.title') }}
-
-
- | {{ $t('falukant.family.relationships.name') }} |
-
+
+
+ - {{ $t('falukant.family.relationships.name') }}
+ -
{{ $t('falukant.titles.' + relationships[0].character2.gender + '.' +
relationships[0].character2.nobleTitle) }}
{{ relationships[0].character2.firstName }}
-
|
-
-
- | {{ $t('falukant.family.spouse.age') }} |
- {{ relationships[0].character2.age }} |
-
-
- | {{ $t('falukant.family.spouse.mood') }} |
- {{ relationships[0].character2.mood?.tr ? $t(`falukant.mood.${relationships[0].character2.mood.tr}`) : '—' }} |
-
-
- | {{ $t('falukant.family.spouse.status') }} |
- {{ $t('falukant.family.statuses.' + relationships[0].relationshipType) }} |
-
-
- | {{ $t('falukant.family.spouse.marriageSatisfaction') }} |
-
+
+
+
+ {{ $t('falukant.family.spouse.age') }}
+ {{ relationships[0].character2.age }}
+
+
+ {{ $t('falukant.family.spouse.mood') }}
+ {{ relationships[0].character2.mood?.tr ? $t(`falukant.mood.${relationships[0].character2.mood.tr}`) : '—' }}
+
+
+ {{ $t('falukant.family.spouse.status') }}
+ {{ $t('falukant.family.statuses.' + relationships[0].relationshipType) }}
+
+
+ {{ $t('falukant.family.spouse.marriageSatisfaction') }}
+
{{ relationships[0].marriageSatisfaction }}
{{ $t('falukant.family.marriageState.' + (relationships[0].marriageState || 'stable')) }}
- |
-
-
- | {{ $t('falukant.family.spouse.progress') }} |
-
+
+
+
+ {{ $t('falukant.family.spouse.progress') }}
+
- |
-
-
-
-
-
-
- -
- {{ $t(`falukant.character.${trait.tr}`) }}
-
-
+
+
+
+
+
+
+
+ {{ $t('falukant.family.spouse.traitsToggle') }}
+
+ -
+ {{ $t(`falukant.character.${trait.tr}`) }}
+
+
+
+
+
{{ $t('falukant.family.children.title') }}
@@ -262,7 +322,13 @@
{{ $t('falukant.family.children.none') }}
+
+
{{ $t('falukant.family.lovers.title') }}
@@ -367,6 +433,7 @@
{{ $t('falukant.family.lovers.candidates.none') }}
+
@@ -417,11 +484,27 @@ export default {
},
pregnancy: null,
selectedChild: null,
- pendingFamilyRefresh: null
+ pendingFamilyRefresh: null,
+ familyTab: 'partner',
}
},
computed: {
- ...mapState(['socket', 'daemonSocket', 'user'])
+ ...mapState(['socket', 'daemonSocket', 'user']),
+ partnerSummaryLine() {
+ if (this.relationships?.length > 0) {
+ const r = this.relationships[0];
+ const c2 = r.character2;
+ const name = c2
+ ? `${this.$t(`falukant.titles.${c2.gender}.${c2.nobleTitle}`)} ${c2.firstName}`.trim()
+ : '';
+ const status = this.$t(`falukant.family.statuses.${r.relationshipType}`);
+ return name ? `${name} · ${status}` : status;
+ }
+ if (this.proposals?.length > 0) {
+ return this.$t('falukant.family.summary.proposalsAvailable');
+ }
+ return this.$t('falukant.family.summary.noPartner');
+ },
},
watch: {
socket(newVal, oldVal) {
@@ -725,26 +808,6 @@ export default {
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 });
@@ -955,6 +1018,116 @@ export default {
color: var(--color-text-secondary);
}
+.family-summary-strip {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-bottom: 12px;
+ padding: 12px 14px;
+}
+
+.family-summary-chip {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 2px;
+ padding: 10px 14px;
+ border-radius: var(--radius-md);
+ border: 1px solid var(--color-border);
+ background: rgba(255, 252, 247, 0.95);
+ cursor: pointer;
+ text-align: left;
+ font: inherit;
+ color: inherit;
+ min-width: 0;
+ flex: 1 1 140px;
+ max-width: 100%;
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
+}
+
+.family-summary-chip:hover {
+ border-color: rgba(248, 162, 43, 0.45);
+}
+
+.family-summary-chip.is-active {
+ border-color: rgba(248, 162, 43, 0.65);
+ box-shadow: 0 0 0 1px rgba(248, 162, 43, 0.2);
+}
+
+.family-summary-chip__label {
+ font-size: 0.72rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--color-text-secondary);
+}
+
+.family-summary-chip__value {
+ font-size: 0.92rem;
+ font-weight: 600;
+ line-height: 1.35;
+ overflow-wrap: anywhere;
+}
+
+.family-tab-panel {
+ margin-bottom: 8px;
+}
+
+.spouse-card__dl {
+ margin: 0;
+ display: grid;
+ gap: 10px 16px;
+}
+
+.spouse-card__dl > div {
+ display: grid;
+ grid-template-columns: minmax(7rem, 34%) 1fr;
+ gap: 8px 12px;
+ align-items: baseline;
+}
+
+.spouse-card__dl dt {
+ margin: 0;
+ color: var(--color-text-secondary);
+ font-size: 0.82rem;
+ font-weight: 600;
+}
+
+.spouse-card__dl dd {
+ margin: 0;
+ font-weight: 600;
+}
+
+.spouse-card__cta {
+ margin-top: 12px;
+}
+
+.spouse-traits {
+ margin-top: 14px;
+ padding: 10px 12px;
+ border: 1px dashed var(--color-border);
+ border-radius: var(--radius-md);
+ background: rgba(255, 250, 243, 0.6);
+}
+
+.spouse-traits summary {
+ cursor: pointer;
+ font-weight: 600;
+ font-size: 0.88rem;
+ color: var(--color-text-secondary);
+}
+
+.spouse-traits ul {
+ margin: 10px 0 0;
+ padding-left: 1.1rem;
+}
+
+@media (max-width: 480px) {
+ .spouse-card__dl > div {
+ grid-template-columns: 1fr;
+ }
+}
+
.marriage-overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
@@ -1335,17 +1508,6 @@ export default {
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;