diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index 3c2efcc..38ae6a5 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -7328,7 +7328,7 @@ ORDER BY r.id`, // Enrich notifications: parse JSON payloads and resolve character names await enrichNotificationsWithCharacterNames(notifications); - return notifications; + return notifications.map(serializeNotificationForClient); } async getAllNotifications(hashedUserId, page = 1, size = 10) { @@ -7344,7 +7344,12 @@ ORDER BY r.id`, await enrichNotificationsWithCharacterNames(rows); - return { items: rows, total: count, page: Number(page) || 1, size: limit }; + return { + items: rows.map(serializeNotificationForClient), + total: count, + page: Number(page) || 1, + size: limit, + }; } async markNotificationsShown(hashedUserId) { @@ -8536,6 +8541,14 @@ ORDER BY r.id`, export default new FalukantService(); +/** Stellt sicher, dass Anreicherungen (z. B. region_name ohne DB-Spalte) im API-JSON landen. */ +function serializeNotificationForClient(row) { + const j = typeof row.toJSON === 'function' ? row.toJSON() : { ...row }; + const dv = row.dataValues || {}; + if (dv.region_name != null && j.region_name == null) j.region_name = dv.region_name; + return j; +} + // Helper: parse notifications for character references and attach characterName async function enrichNotificationsWithCharacterNames(notifications) { if (!Array.isArray(notifications) || notifications.length === 0) return; @@ -8552,7 +8565,7 @@ async function enrichNotificationsWithCharacterNames(notifications) { if (typeof obj !== 'object') return; for (const [k, v] of Object.entries(obj)) { if (!v) continue; - if (k === 'character_id' || k === 'characterId') { + if (k === 'character_id' || k === 'characterId' || k === 'director_character_id') { charIds.add(Number(v)); continue; } @@ -8567,15 +8580,25 @@ async function enrichNotificationsWithCharacterNames(notifications) { } } - // First pass: collect all referenced character ids from notifications + const regionIds = new Set(); + + // First pass: collect all referenced character ids and region ids from notifications for (const n of notifications) { - // parse n.tr if it's JSON + let parsed = null; try { if (typeof n.tr === 'string' && n.tr.trim().startsWith('{')) { - const parsed = JSON.parse(n.tr); + parsed = JSON.parse(n.tr); + collectIds(parsed); + } else if (n.tr && typeof n.tr === 'object') { + parsed = n.tr; collectIds(parsed); } - } catch (err) { /* ignore */ } + } catch (err) { + parsed = null; + } + if (parsed?.region_id != null) { + regionIds.add(Number(parsed.region_id)); + } // parse n.effects if present try { @@ -8591,24 +8614,34 @@ async function enrichNotificationsWithCharacterNames(notifications) { } const ids = Array.from(charIds).filter(Boolean); - if (!ids.length) return; - - // Batch load characters and their display names - const characters = await FalukantCharacter.findAll({ - where: { id: { [Op.in]: ids } }, - include: [ - { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }, - { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] } - ], - attributes: ['id'] - }); + const regionIdList = Array.from(regionIds).filter((id) => Number.isFinite(id)); + if (!ids.length && !regionIdList.length) return; const nameMap = new Map(); - for (const c of characters) { - const first = c.definedFirstName?.name || ''; - const last = c.definedLastName?.name || ''; - const display = `${first} ${last}`.trim() || null; - nameMap.set(Number(c.id), display || `#${c.id}`); + if (ids.length) { + const characters = await FalukantCharacter.findAll({ + where: { id: { [Op.in]: ids } }, + include: [ + { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }, + { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] } + ], + attributes: ['id'] + }); + + for (const c of characters) { + const first = c.definedFirstName?.name || ''; + const last = c.definedLastName?.name || ''; + const display = `${first} ${last}`.trim() || null; + nameMap.set(Number(c.id), display || `#${c.id}`); + } + } + let regionNameById = new Map(); + if (regionIdList.length) { + const regions = await RegionData.findAll({ + where: { id: { [Op.in]: regionIdList } }, + attributes: ['id', 'name'], + }); + regionNameById = new Map(regions.map((r) => [Number(r.id), r.name])); } // helper to find first character id in an object @@ -8625,6 +8658,7 @@ async function enrichNotificationsWithCharacterNames(notifications) { for (const [k, v] of Object.entries(obj)) { if (!v) continue; if (k === 'character_id' || k === 'characterId') return Number(v); + if (k === 'director_character_id') return Number(v); if (k === 'character' && typeof v === 'object') { if (v.id) return Number(v.id); const r = findFirstId(v); @@ -8638,10 +8672,23 @@ async function enrichNotificationsWithCharacterNames(notifications) { // Attach resolved name to notifications (set character_name; characterName is a getter that reads from it) for (const n of notifications) { - let foundId = null; + let parsed = null; try { if (typeof n.tr === 'string' && n.tr.trim().startsWith('{')) { - const parsed = JSON.parse(n.tr); + parsed = JSON.parse(n.tr); + } else if (n.tr && typeof n.tr === 'object') { + parsed = n.tr; + } + } catch (err) { + parsed = null; + } + + let foundId = null; + if (parsed?.director_character_id != null) { + foundId = Number(parsed.director_character_id); + } + try { + if (!foundId && parsed) { foundId = findFirstId(parsed) || foundId; } } catch (err) { /* ignore */ } @@ -8661,5 +8708,11 @@ async function enrichNotificationsWithCharacterNames(notifications) { // Set character_name directly (characterName is a getter that reads from character_name) n.character_name = resolved; } + + if (parsed?.region_id != null && regionNameById.has(Number(parsed.region_id))) { + const rn = regionNameById.get(Number(parsed.region_id)); + // Kein DB-Feld: explizit in dataValues setzen, damit toJSON() es mitschickt + if (n.dataValues) n.dataValues.region_name = rn; + } } } diff --git a/frontend/src/components/falukant/MessagesDialog.vue b/frontend/src/components/falukant/MessagesDialog.vue index 8763f9c..1f30c6e 100644 --- a/frontend/src/components/falukant/MessagesDialog.vue +++ b/frontend/src/components/falukant/MessagesDialog.vue @@ -138,6 +138,9 @@ export default { if (!n || typeof n !== 'object') return n || {}; let merged = { ...n }; const raw = n.tr; + if (raw && typeof raw === 'object') { + merged = { ...merged, ...raw }; + } if (typeof raw === 'string') { const trimmed = raw.trim(); if (trimmed.startsWith('{') && trimmed.endsWith('}')) { @@ -151,6 +154,9 @@ export default { } } } + if (merged.character_name && merged.characterName == null) { + merged.characterName = merged.character_name; + } return merged; }, @@ -228,10 +234,8 @@ export default { try { const titleKey = `${eventKey}.title`; const descKey = `${eventKey}.description`; - // If no params were parsed from JSON, try to extract them from the notification (effects, character_id, etc.) - if ((!params || Object.keys(params).length === 0) && payload) { - params = this.extractParams(payload) || {}; - } + const extracted = this.extractParams(payload) || {}; + params = { ...extracted, ...params }; if (this.$te(titleKey) && this.$te(descKey)) { const title = this.$t(titleKey, params); @@ -243,6 +247,10 @@ export default { } } + // Immer Parameter aus dem vollständigen Payload (nach merge), z. B. director.resignation_risk_high + const extractedAll = this.extractParams(payload) || {}; + params = { ...extractedAll, ...params }; + // Fallback: Alte Methode für andere Notification-Typen return this.$t(key, params); }, @@ -312,6 +320,14 @@ export default { extractParams(n) { const base = this.mergeNotificationPayload(n); const params = {}; + const locale = this.$i18n?.locale || 'de'; + const isGerman = String(locale).startsWith('de'); + + if (base.characterName) { + params.characterName = base.characterName; + } else if (base.character_name) { + params.characterName = base.character_name; + } // Parameter aus effects extrahieren (Daemon: effects oft nur im JSON in tr) if (base.effects && Array.isArray(base.effects)) { @@ -326,11 +342,27 @@ export default { } else if (effect.percent !== undefined) { params.percent = effect.percent; } + } else if (effect.type === 'price_change') { + if (effect.percent !== undefined && effect.percent !== null) { + const p = Number(effect.percent); + if (Number.isFinite(p)) { + params.priceChangePercent = `${p > 0 ? '+' : ''}${p.toFixed(1)}`; + } + } + } else if (effect.type === 'production_quality_change') { + if (effect.change !== undefined && effect.change !== null) { + const v = Number(effect.change); + if (Number.isFinite(v)) { + params.productionQualityChange = (v > 0 ? '+' : '') + (Number.isInteger(v) ? String(v) : v.toFixed(1)); + } + } + } else if (effect.type === 'weather_change') { + params.hasWeatherChange = true; } else if (effect.type === 'character_health_change') { if (effect.character_id) { // Prefer explicit characterName from notification, otherwise fall back to provided name or use id placeholder params.character_id = effect.character_id; - params.characterName = params.characterName || base.characterName || `#${effect.character_id}`; + params.characterName = params.characterName || base.characterName || base.character_name || `#${effect.character_id}`; } if (effect.change !== undefined) { params.change = effect.change; @@ -352,9 +384,33 @@ export default { } // Weitere Parameter aus der Notification selbst - if (base.region_id && base.regionName) { - params.regionName = base.regionName; + if (base.region_id != null) { + if (base.regionName) { + params.regionName = base.regionName; + } else if (base.region_name) { + params.regionName = base.region_name; + } } + + // Daemon: Meta-Objekte (auch wenn effects[] schon Werte liefert — fehlende Werte ergänzen) + if (params.priceChangePercent == null && base.price_change && typeof base.price_change === 'object') { + const applied = base.price_change.applied !== false; + const pct = base.price_change.percent; + if (applied && pct != null && Number.isFinite(Number(pct))) { + const p = Number(pct); + params.priceChangePercent = `${p > 0 ? '+' : ''}${p.toFixed(1)}`; + } + } + if (params.productionQualityChange == null && base.production_quality && typeof base.production_quality === 'object') { + const applied = base.production_quality.applied !== false; + const ch = base.production_quality.change; + if (applied && ch != null && Number.isFinite(Number(ch))) { + const v = Number(ch); + params.productionQualityChange = (v > 0 ? '+' : '') + (Number.isInteger(v) ? String(v) : v.toFixed(1)); + } + } + + params.priceEffectLine = this.buildRegionalFestivalEffectsLine(params, isGerman); if (base.character_id && base.characterName) { params.characterName = base.characterName; } @@ -370,6 +426,13 @@ export default { if (base.threshold_percent !== undefined) { params.threshold_percent = base.threshold_percent; } + const isDirectorResign = base.event === 'director_resignation_risk_high' + || (typeof base.tr === 'string' && base.tr.includes('director.resignation')); + if (isDirectorResign) { + params.directorName = params.characterName + || base.character_name + || (base.director_character_id != null ? `#${base.director_character_id}` : ''); + } if (base.director_id !== undefined) { params.director_id = base.director_id; } @@ -440,7 +503,48 @@ export default { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(amount); - } + }, + + /** + * Zeilen für regionale Events (Fest o. ä.): Wetter, Preis, Produktionsqualität — aus effects[] und/oder price_change / production_quality. + */ + buildRegionalFestivalEffectsLine(params, isGerman) { + const parts = []; + const kw = 'falukant.notifications.random_event.effects.festival_weather_line'; + const kp = 'falukant.notifications.random_event.effects.festival_price_line'; + const kq = 'falukant.notifications.random_event.effects.festival_quality_line'; + if (params.hasWeatherChange) { + if (this.$te(kw)) parts.push(String(this.$t(kw))); + else parts.push(isGerman ? 'Wetter in der Region schlägt um.' : 'The regional weather shifts.'); + } + if (params.priceChangePercent != null) { + if (this.$te(kp)) { + parts.push(String(this.$t(kp, { percent: params.priceChangePercent }))); + } else { + const legacy = 'falukant.notifications.random_event.effects.price_effect_suffix'; + parts.push( + this.$te(legacy) + ? String(this.$t(legacy, { percent: params.priceChangePercent })) + : (isGerman + ? `Warenpreise etwa ${params.priceChangePercent} %.` + : `Goods prices about ${params.priceChangePercent}%.`) + ); + } + } + if (params.productionQualityChange != null) { + if (this.$te(kq)) { + parts.push(String(this.$t(kq, { change: params.productionQualityChange }))); + } else { + parts.push( + isGerman + ? `Produktionsqualität etwa ${params.productionQualityChange}.` + : `Production quality about ${params.productionQualityChange}.` + ); + } + } + if (!parts.length) return ''; + return ` ${parts.join(' ')}`; + }, }, computed: { totalPages() { diff --git a/frontend/src/i18n/locales/ceb/falukant.json b/frontend/src/i18n/locales/ceb/falukant.json index 56ae802..658e9a4 100644 --- a/frontend/src/i18n/locales/ceb/falukant.json +++ b/frontend/src/i18n/locales/ceb/falukant.json @@ -35,7 +35,7 @@ "notify_election_created": "Giskedyul ang usa ka bag-ong eleksiyon.", "notify_office_filled": "Na puno ang usa ka politikal nga opisina.", "director": { - "resignation_risk_high": "Taas ang risgo nga mubiyaa ang direktor: risgo {risk_percent}% (threshold {threshold_percent}%). Karon nga satisfaction {satisfaction}." + "resignation_risk_high": "Taas ang risgo nga mobiya ang direktor nga si {directorName}: risgo {risk_percent}% (threshold {threshold_percent}%). Karon nga satisfaction {satisfaction}." }, "director_death": "Namatay si {characterName} sa edad nga {ageYears}. Isip amo, kinahanglan kang magtudlo og bag-ong direktor.{regionLabel}{spouses}{children}{lovers}", "relationship_death": "Namatay si {characterName} sa edad nga {ageYears}.{regionLabel}{spouses}{children}{lovers}", @@ -93,7 +93,13 @@ }, "regional_festival": { "title": "Pista sa rehiyon", - "description": "Adunay dakong pista sa rehiyon nga {regionName}." + "description": "Adunay dakong pista sa rehiyon nga {regionName}.{priceEffectLine}" + }, + "effects": { + "price_effect_suffix": " Epekto: gipaabot nga pagbag-o sa presyo sa mga paliton nga ~{percent}%.", + "festival_weather_line": "Ang panahon sa rehiyon nagbag-o.", + "festival_price_line": "Presyo sa mga paliton ~{percent}%.", + "festival_quality_line": "Kalidad sa produksyon ~{change}." }, "regional_epidemic": { "title": "Epidemya", diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json index 28b2497..dea70ce 100644 --- a/frontend/src/i18n/locales/de/falukant.json +++ b/frontend/src/i18n/locales/de/falukant.json @@ -49,7 +49,7 @@ "notify_election_created": "Es wurde eine neue Wahl ausgeschrieben.", "notify_office_filled": "Ein politisches Amt wurde neu besetzt.", "director": { - "resignation_risk_high": "Hohe Kündigungsgefahr bei einem Direktor: Risiko {risk_percent}% (Schwelle {threshold_percent}%). Zufriedenheit aktuell {satisfaction}." + "resignation_risk_high": "Hohe Kündigungsgefahr für den Direktor {directorName}: Risiko {risk_percent} % (Schwelle {threshold_percent} %). Zufriedenheit aktuell {satisfaction}." }, "director_death": "{characterName} ist im Alter von {ageYears} Jahren verstorben. Als Arbeitgeber musst du die Direktion neu besetzen.{regionLabel}{spouses}{children}{lovers}", "relationship_death": "{characterName} ist im Alter von {ageYears} Jahren verstorben.{regionLabel}{spouses}{children}{lovers}", @@ -95,7 +95,13 @@ }, "regional_festival": { "title": "Regionales Fest", - "description": "Ein großes Fest findet in der Region {regionName} statt." + "description": "Ein großes Fest findet in der Region {regionName} statt.{priceEffectLine}" + }, + "effects": { + "price_effect_suffix": " Auswirkung: erwartete Warenpreisänderung ca. {percent} %.", + "festival_weather_line": "Wetter in der Region schlägt um.", + "festival_price_line": "Warenpreise etwa {percent} %.", + "festival_quality_line": "Produktionsqualität etwa {change}." }, "regional_epidemic": { "title": "Epidemie", diff --git a/frontend/src/i18n/locales/en/falukant.json b/frontend/src/i18n/locales/en/falukant.json index d1ad4e7..72851e8 100644 --- a/frontend/src/i18n/locales/en/falukant.json +++ b/frontend/src/i18n/locales/en/falukant.json @@ -35,7 +35,7 @@ "notify_election_created": "A new election has been scheduled.", "notify_office_filled": "A political office has been filled.", "director": { - "resignation_risk_high": "High resignation risk for a director: risk {risk_percent}% (threshold {threshold_percent}%). Current satisfaction {satisfaction}." + "resignation_risk_high": "High resignation risk for director {directorName}: risk {risk_percent}% (threshold {threshold_percent}%). Current satisfaction {satisfaction}." }, "director_death": "{characterName} died at the age of {ageYears}. As employer you need to appoint a new director.{regionLabel}{spouses}{children}{lovers}", "relationship_death": "{characterName} died at the age of {ageYears}.{regionLabel}{spouses}{children}{lovers}", @@ -93,7 +93,13 @@ }, "regional_festival": { "title": "Regional Festival", - "description": "A large festival is taking place in the region {regionName}." + "description": "A large festival is taking place in the region {regionName}.{priceEffectLine}" + }, + "effects": { + "price_effect_suffix": " Effect: expected goods price change ~{percent}%.", + "festival_weather_line": "The regional weather shifts.", + "festival_price_line": "Goods prices about {percent}%.", + "festival_quality_line": "Production quality about {change}." }, "regional_epidemic": { "title": "Epidemic", diff --git a/frontend/src/i18n/locales/es/falukant.json b/frontend/src/i18n/locales/es/falukant.json index ef5315b..ee8d25a 100644 --- a/frontend/src/i18n/locales/es/falukant.json +++ b/frontend/src/i18n/locales/es/falukant.json @@ -49,7 +49,7 @@ "notify_election_created": "Se ha convocado una nueva elección.", "notify_office_filled": "Se ha cubierto un cargo político.", "director": { - "resignation_risk_high": "Alto riesgo de renuncia de un director: riesgo {risk_percent}% (umbral {threshold_percent}%). Satisfacción actual {satisfaction}." + "resignation_risk_high": "Alto riesgo de renuncia del director {directorName}: riesgo {risk_percent}% (umbral {threshold_percent}%). Satisfacción actual {satisfaction}." }, "director_death": "{characterName} ha fallecido a la edad de {ageYears} años. Como empleador debes nombrar un nuevo director.{regionLabel}{spouses}{children}{lovers}", "relationship_death": "{characterName} ha fallecido a la edad de {ageYears} años.{regionLabel}{spouses}{children}{lovers}", @@ -95,7 +95,13 @@ }, "regional_festival": { "title": "Fiesta regional", - "description": "Se celebra una gran fiesta en la región {regionName}." + "description": "Se celebra una gran fiesta en la región {regionName}.{priceEffectLine}" + }, + "effects": { + "price_effect_suffix": " Efecto: variación esperada del precio de las mercancías ~{percent}%.", + "festival_weather_line": "El tiempo en la región cambia.", + "festival_price_line": "Precios de mercancías ~{percent} %.", + "festival_quality_line": "Calidad de producción ~{change}." }, "regional_epidemic": { "title": "Epidemia", diff --git a/frontend/src/i18n/locales/fr/falukant.json b/frontend/src/i18n/locales/fr/falukant.json index 1aa9115..d6c1cc5 100644 --- a/frontend/src/i18n/locales/fr/falukant.json +++ b/frontend/src/i18n/locales/fr/falukant.json @@ -49,7 +49,7 @@ "notify_election_created": "Une nouvelle élection a été déclenchée.", "notify_office_filled": "Une fonction politique a été pourvue.", "director": { - "resignation_risk_high": "Risque élevé de démission d’un directeur : risque {risk_percent}% (seuil {threshold_percent}%). Satisfaction actuelle {satisfaction}." + "resignation_risk_high": "Risque élevé de démission du directeur {directorName} : risque {risk_percent}% (seuil {threshold_percent}%). Satisfaction actuelle {satisfaction}." }, "director_death": "{characterName} est décédé à l'âge de {ageYears}. En tant qu'employeur, vous devez remplir le conseil d'administration.{regionLabel}{spouses}{children}{lovers}", "relationship_death": "{characterName} est décédé à l'âge de {ageYears}.{regionLabel}{spouses}{children}{lovers}", @@ -95,7 +95,13 @@ }, "regional_festival": { "title": "Fête régionale", - "description": "Un grand festival a lieu dans la région {regionName}." + "description": "Un grand festival a lieu dans la région {regionName}.{priceEffectLine}" + }, + "effects": { + "price_effect_suffix": " Effet : variation attendue des prix des marchandises ~{percent} %.", + "festival_weather_line": "Le temps dans la région change.", + "festival_price_line": "Prix des marchandises ~{percent} %.", + "festival_quality_line": "Qualité de production ~{change}." }, "regional_epidemic": { "title": "Épidémie",