Files
yourpart3/frontend/src/components/falukant/MessagesDialog.vue

367 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<DialogWidget
ref="dlg"
name="falukant-messages"
:title="'falukant.messages.title'"
:isTitleTranslated="true"
icon="falukant/messages24.png"
:buttons="[
{ text: 'falukant.messages.markAllRead', action: 'markAll' },
{ text: 'message.close', action: 'close' }
]"
width="520px"
height="420px"
>
<ul class="messages">
<li v-for="n in messages" :key="n.id" :class="{ unread: !n.shown }">
<div class="body">
<div v-if="formatBody(n).title" class="notification-title">{{ formatBody(n).title }}</div>
<div class="notification-description">{{ formatBody(n).description || formatBody(n) }}</div>
</div>
<div class="footer">
<span>{{ formatDate(n.createdAt) }}</span>
</div>
</li>
<li v-if="messages.length === 0" class="empty">{{ $t('falukant.messages.empty') }}</li>
</ul>
<div class="pagination" v-if="total > size">
<button @click="firstPage" :disabled="!canPrev">«</button>
<button @click="prevPage" :disabled="!canPrev"></button>
<span>
<input type="number" min="1" :max="totalPages" v-model.number="pageInput" @change="goToPage(pageInput)" />
/ {{ totalPages }}
</span>
<button @click="nextPage" :disabled="!canNext"></button>
<button @click="lastPage" :disabled="!canNext">»</button>
</div>
</DialogWidget>
</template>
<script>
import DialogWidget from '@/components/DialogWidget.vue';
import apiClient from '@/utils/axios.js';
export default {
name: 'FalukantMessagesDialog',
components: { DialogWidget },
data() {
return {
messages: [],
page: 1,
size: 10,
total: 0,
pageInput: 1,
};
},
methods: {
async open() {
this.page = 1;
this.pageInput = 1;
await this.load();
this.$refs.dlg.open();
// mark unread as shown
try { await apiClient.post('/api/falukant/notifications/mark-shown'); } catch {}
},
async markAll() {
try {
await apiClient.post('/api/falukant/notifications/mark-shown');
// reload to update shown flags and unread count
await this.load();
} catch (e) {
// ignore errors silently
}
},
async load() {
try {
const { data } = await apiClient.get('/api/falukant/notifications/all', { params: { page: this.page, size: this.size } });
if (data && Array.isArray(data.items)) {
this.messages = data.items;
this.total = data.total || 0;
} else {
this.messages = Array.isArray(data) ? data : [];
this.total = this.messages.length;
}
} catch (e) {
console.error('Failed to load messages', e);
this.messages = [];
this.total = 0;
}
},
nextPage() {
if (this.page < this.totalPages) {
this.page++;
this.pageInput = this.page;
this.load();
}
},
prevPage() {
if (this.page > 1) {
this.page--;
this.pageInput = this.page;
this.load();
}
},
firstPage() {
if (this.page !== 1) {
this.page = 1;
this.pageInput = 1;
this.load();
}
},
lastPage() {
if (this.page !== this.totalPages) {
this.page = this.totalPages;
this.pageInput = this.page;
this.load();
}
},
goToPage(val) {
let v = Number(val) || 1;
if (v < 1) v = 1;
if (v > this.totalPages) v = this.totalPages;
if (v !== this.page) {
this.page = v;
this.pageInput = v;
this.load();
} else {
this.pageInput = v;
}
},
formatDate(dt) {
try {
return new Date(dt).toLocaleString();
} catch { return dt; }
},
formatBody(n) {
// Wenn die Notification bereits title und description hat (z.B. von WebSocket Events)
if (n.title && n.description) {
// Parameter aus effects oder anderen Feldern extrahieren
const params = this.extractParams(n);
return {
title: this.interpolateString(n.title, params),
description: this.interpolateString(n.description, params)
};
}
let raw = n.tr || '';
let key = raw;
let params = {};
// 1) JSON-Format unterstützen: {"tr":"random_event.windfall","amount":1000,"characterName":"Max"}
if (typeof raw === 'string') {
const trimmed = raw.trim();
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
try {
const parsed = JSON.parse(trimmed);
if (parsed && parsed.tr) {
raw = parsed.tr;
key = parsed.tr;
// Alle anderen Felder als Parameter verwenden und formatieren
params = this.formatParams({ ...parsed });
delete params.tr;
// Merge in params extracted from nested structures (effects, character ids)
try {
const extracted = this.extractParams({ ...n, ...parsed, characterName: parsed.characterName || parsed.character_name || n.characterName || n.character_name });
for (const [k, v] of Object.entries(extracted || {})) {
if (!params.hasOwnProperty(k) || params[k] === undefined || params[k] === null || params[k] === '') {
params[k] = v;
}
}
} catch (e) {
// ignore extraction errors
}
}
} catch (e) {
// bei Parse-Fehler einfach weiter unten mit dem Rohwert arbeiten
}
}
}
// 2) Schlüssel normalisieren:
// - wenn bereits ein voller i18n-Key wie "falukant.notifications.production.overproduction",
// dann direkt verwenden
// - sonst in den Namespace "falukant.notifications." hängen
if (typeof key === 'string') {
const trimmedKey = key.trim();
if (trimmedKey.startsWith('falukant.')) {
key = trimmedKey;
} else {
key = 'falukant.notifications.' + trimmedKey;
}
}
// 3) Prüfe, ob es sich um ein random_event handelt mit title/description Struktur
if (key.startsWith('falukant.notifications.random_event.')) {
const eventId = key.replace('falukant.notifications.random_event.', '');
const eventKey = `falukant.notifications.random_event.${eventId}`;
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) && n) {
params = this.extractParams(n) || {};
}
if (this.$te(titleKey) && this.$te(descKey)) {
const title = this.$t(titleKey, params);
const description = this.$t(descKey, params);
return { title, description };
}
} catch (e) {
// Fallback zur alten Methode
}
}
// Fallback: Alte Methode für andere Notification-Typen
return this.$t(key, params);
},
formatParams(params) {
const formatted = {};
// Geldbeträge formatieren
if (params.amount !== undefined) {
formatted.amount = this.formatMoney(params.amount);
}
if (params.absolute !== undefined) {
formatted.amount = this.formatMoney(params.absolute);
}
if (params.percent !== undefined) {
formatted.amount = `${params.percent > 0 ? '+' : ''}${params.percent.toFixed(1)}%`;
}
// Gesundheit formatieren
if (params.change !== undefined) {
formatted.healthChange = params.change > 0 ? `+${params.change}` : `${params.change}`;
}
if (params.healthChange !== undefined) {
formatted.healthChange = params.healthChange > 0 ? `+${params.healthChange}` : `${params.healthChange}`;
}
// Schaden formatieren
if (params.inventory_damage_percent !== undefined) {
formatted.damagePercent = ` Lagerbestand beschädigt: ${params.inventory_damage_percent.toFixed(1)}%.`;
}
if (params.storage_destruction_percent !== undefined) {
formatted.destructionPercent = ` Lager zerstört: ${params.storage_destruction_percent.toFixed(1)}%.`;
}
// Alle anderen Parameter übernehmen
for (const [key, value] of Object.entries(params)) {
if (!formatted.hasOwnProperty(key) && key !== 'tr') {
formatted[key] = value;
}
}
return formatted;
},
extractParams(n) {
const params = {};
// Parameter aus effects extrahieren
if (n.effects && Array.isArray(n.effects)) {
for (const effect of n.effects) {
if (effect.type === 'money_change') {
if (effect.absolute !== undefined) {
params.amount = effect.absolute;
} else if (effect.percent !== undefined) {
params.percent = effect.percent;
}
} 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 || n.characterName || `#${effect.character_id}`;
}
if (effect.change !== undefined) {
params.change = effect.change;
}
} else if (effect.type === 'character_death') {
if (effect.character_id) {
params.character_id = effect.character_id;
params.characterName = params.characterName || n.characterName || `#${effect.character_id}`;
}
} else if (effect.type === 'storage_damage') {
if (effect.inventory_damage_percent !== undefined) {
params.inventory_damage_percent = effect.inventory_damage_percent;
}
if (effect.storage_destruction_percent !== undefined) {
params.storage_destruction_percent = effect.storage_destruction_percent;
}
}
}
}
// Weitere Parameter aus der Notification selbst
if (n.region_id && n.regionName) {
params.regionName = n.regionName;
}
if (n.character_id && n.characterName) {
params.characterName = n.characterName;
}
if (n.amount !== undefined) {
params.amount = n.amount;
}
return this.formatParams(params);
},
interpolateString(str, params) {
if (!str || typeof str !== 'string') return str;
let result = str;
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null && value !== '') {
result = result.replace(new RegExp(`\\{${key}\\}`, 'g'), String(value));
} else {
// Entferne Platzhalter, wenn kein Wert vorhanden ist
result = result.replace(new RegExp(`\\s*\\{${key}\\}\\s*`, 'g'), '');
}
}
// Entferne doppelte Leerzeichen und trimme
result = result.replace(/\s+/g, ' ').trim();
return result;
},
formatMoney(amount) {
return new Intl.NumberFormat(navigator.language, {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(amount);
}
},
computed: {
totalPages() {
return Math.max(1, Math.ceil(this.total / this.size));
},
canPrev() { return this.page > 1; },
canNext() { return this.page < this.totalPages; }
}
};
</script>
<style scoped>
.messages {
list-style: none;
padding: 0;
margin: 0;
}
.messages > li { border: 1px solid #7BBE55; margin-bottom: .25em; padding: .5em; }
.messages > li.unread { font-weight: bold; }
.messages > li .body { display: flex; flex-direction: column; gap: 0.25em; }
.messages > li .notification-title { font-weight: bold; font-size: 1.05em; }
.messages > li .notification-description { color: #555; }
.messages > li .footer { color: #f9a22c; font-size: .8em; margin-top: .3em; display: flex; }
.messages > li .footer span:first-child { flex: 1; }
.empty { text-align: center; color: #777; padding: 1em; }
.pagination {
display: flex;
justify-content: center;
gap: .5em;
margin-top: .5em;
}
.pagination button { padding: .25em .6em; }
.pagination input[type="number"] { width: 4em; text-align: right; }
</style>