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;