diff --git a/public/i18n.js b/public/i18n.js index c14da31..64b88d9 100644 --- a/public/i18n.js +++ b/public/i18n.js @@ -8,6 +8,8 @@ const SUPPORTED_LOCALES = ['de', 'en', 'es', 'fr', 'it', 'sv', 'el', 'ru', 'tr', 'zh', 'ja', 'ar', 'hi', 'pt', 'uk']; const DEFAULT_LOCALE = 'de'; const STORAGE_KEY = 'oikos-locale'; +const DATE_FORMAT_KEY = 'oikos-date-format'; +const DEFAULT_DATE_FORMAT = 'mdy'; let currentLocale = DEFAULT_LOCALE; let translations = {}; @@ -78,6 +80,28 @@ export function t(key, params = {}) { return str; } +function isDateOnlyString(value) { + return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value); +} + +function getDateFormatPreference() { + const stored = localStorage.getItem(DATE_FORMAT_KEY); + return ['mdy', 'dmy', 'ymd'].includes(stored) ? stored : DEFAULT_DATE_FORMAT; +} + +function formatDateParts(date, useUtc = false) { + const d = date instanceof Date ? date : new Date(date); + if (isNaN(d.getTime())) return ''; + const year = useUtc ? d.getUTCFullYear() : d.getFullYear(); + const month = String((useUtc ? d.getUTCMonth() : d.getMonth()) + 1).padStart(2, '0'); + const day = String(useUtc ? d.getUTCDate() : d.getDate()).padStart(2, '0'); + switch (getDateFormatPreference()) { + case 'dmy': return `${day}/${month}/${year}`; + case 'ymd': return `${year}-${month}-${day}`; + default: return `${month}/${day}/${year}`; + } +} + /** Aktuelle Locale abfragen */ export function getLocale() { return currentLocale; @@ -91,13 +115,10 @@ export function getSupportedLocales() { /** Datum locale-aware formatieren */ export function formatDate(date) { if (date == null) return ''; - const d = date instanceof Date ? date : new Date(date); - if (isNaN(d.getTime())) return ''; - return new Intl.DateTimeFormat(currentLocale, { - day: '2-digit', - month: '2-digit', - year: 'numeric', - }).format(d); + if (isDateOnlyString(date)) { + return formatDateParts(new Date(`${date}T00:00:00Z`), true); + } + return formatDateParts(date); } /** Uhrzeit locale-aware formatieren */ diff --git a/public/locales/ar.json b/public/locales/ar.json index 38fa584..be87365 100644 --- a/public/locales/ar.json +++ b/public/locales/ar.json @@ -83,6 +83,19 @@ "allDay": "طوال اليوم", "shoppingMore": "+{{count}} أخرى", "weather": "الطقس", + "familyMembers": "أفراد العائلة", + "participantsAdded": "مشاركون مضافون", + "upcomingBirthdays": "أعياد الميلاد القادمة", + "noBirthdays": "لا توجد أعياد ميلاد بعد", + "daysLeft": "{{count}} أيام", + "budgetOverview": "نظرة عامة على الميزانية", + "monthlyIncome": "الدخل", + "monthlyExpenses": "المصروفات", + "monthlyBalance": "الرصيد", + "savingsRate": "معدل الادخار", + "topExpense": "أكبر مصروف", + "budgetEntries": "القيود", + "noBudgetData": "لا توجد بيانات ميزانية لهذا الشهر.", "customize": "تخصيص", "customizeTitle": "تخصيص الأدوات", "customizeReset": "الافتراضي", @@ -537,6 +550,7 @@ "tabAccount": "الحساب", "tabsAriaLabel": "أقسام الإعدادات", "sectionDesign": "التصميم", + "sectionAppName": "اسم التطبيق", "sectionShopping": "التسوق", "shoppingCategoriesLabel": "فئات التسوق", "shoppingCategoriesHint": "إضافة الفئات أو إعادة تسميتها أو حذفها أو ترتيبها.", @@ -554,6 +568,16 @@ "sectionCalendarSync": "مزامنة التقويم", "sectionFamily": "أفراد العائلة", "cardAppearance": "المظهر", + "appNameTitle": "اسم التطبيق", + "appNameLabel": "اسم التطبيق", + "appNameHint": "يظهر هذا الاسم في الشريط الجانبي وعنوان المتصفح وشاشة تسجيل الدخول.", + "appNamePlaceholder": "Oikos", + "appNameSavedToast": "تم حفظ اسم التطبيق.", + "sectionDate": "التاريخ", + "dateFormatTitle": "تنسيق التاريخ", + "dateFormatLabel": "تنسيق التاريخ المفضل", + "dateFormatHint": "اختر كيف تظهر التواريخ في التطبيق.", + "dateFormatSavedToast": "تم حفظ تنسيق التاريخ.", "themeSystem": "النظام", "themeSysLabel": "استخدام إعداد النظام", "themeLight": "فاتح", diff --git a/public/locales/de.json b/public/locales/de.json index 9c36484..291975f 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -89,6 +89,19 @@ "allDay": "Ganztägig", "shoppingMore": "+{{count}} weitere", "weather": "Wetter", + "familyMembers": "Familienmitglieder", + "participantsAdded": "Teilnehmer hinzugefügt", + "upcomingBirthdays": "Nächste Geburtstage", + "noBirthdays": "Noch keine Geburtstage", + "daysLeft": "{{count}} Tage", + "budgetOverview": "Budgetübersicht", + "monthlyIncome": "Einnahmen", + "monthlyExpenses": "Ausgaben", + "monthlyBalance": "Saldo", + "savingsRate": "Sparquote", + "topExpense": "Größte Ausgabe", + "budgetEntries": "Einträge", + "noBudgetData": "Keine Budgetdaten in diesem Monat.", "customize": "Anpassen", "customizeTitle": "Widgets anpassen", "customizeReset": "Standard", @@ -543,6 +556,7 @@ "tabAccount": "Konto", "tabsAriaLabel": "Einstellungsbereiche", "sectionDesign": "Design", + "sectionAppName": "Anwendungsname", "sectionShopping": "Einkauf", "shoppingCategoriesLabel": "Einkaufskategorien", "shoppingCategoriesHint": "Kategorien hinzufügen, umbenennen, löschen oder sortieren.", @@ -560,6 +574,16 @@ "sectionCalendarSync": "Kalender-Synchronisation", "sectionFamily": "Familienmitglieder", "cardAppearance": "Darstellung", + "appNameTitle": "App-Name", + "appNameLabel": "Anwendungsname", + "appNameHint": "Dieser Name erscheint in der Seitenleiste, im Browser-Titel und auf dem Login-Bildschirm.", + "appNamePlaceholder": "Oikos", + "appNameSavedToast": "Anwendungsname gespeichert.", + "sectionDate": "Datum", + "dateFormatTitle": "Datumsformat", + "dateFormatLabel": "Bevorzugtes Datumsformat", + "dateFormatHint": "Wähle, wie Daten in der App angezeigt werden.", + "dateFormatSavedToast": "Datumsformat gespeichert.", "themeSystem": "System", "themeSysLabel": "System-Einstellung verwenden", "themeLight": "Hell", diff --git a/public/locales/el.json b/public/locales/el.json index 9b9c1d7..b882d69 100644 --- a/public/locales/el.json +++ b/public/locales/el.json @@ -83,6 +83,19 @@ "allDay": "Όλη μέρα", "shoppingMore": "+{{count}} ακόμα", "weather": "Καιρός", + "familyMembers": "Μέλη οικογένειας", + "participantsAdded": "συμμετέχοντες προστέθηκαν", + "upcomingBirthdays": "Επόμενα γενέθλια", + "noBirthdays": "Δεν υπάρχουν γενέθλια ακόμα", + "daysLeft": "{{count}} ημέρες", + "budgetOverview": "Επισκόπηση προϋπολογισμού", + "monthlyIncome": "Έσοδα", + "monthlyExpenses": "Έξοδα", + "monthlyBalance": "Υπόλοιπο", + "savingsRate": "Ποσοστό αποταμίευσης", + "topExpense": "Μεγαλύτερο έξοδο", + "budgetEntries": "Καταχωρήσεις", + "noBudgetData": "Δεν υπάρχουν δεδομένα προϋπολογισμού αυτόν τον μήνα.", "customize": "Προσαρμογή", "customizeTitle": "Προσαρμογή widgets", "customizeReset": "Επαναφορά", @@ -537,6 +550,7 @@ "tabAccount": "Λογαριασμός", "tabsAriaLabel": "Τμήματα ρυθμίσεων", "sectionDesign": "Εμφάνιση", + "sectionAppName": "Όνομα εφαρμογής", "sectionShopping": "Αγορές", "shoppingCategoriesLabel": "Κατηγορίες αγορών", "shoppingCategoriesHint": "Προσθέστε, μετονομάστε, διαγράψτε ή ταξινομήστε κατηγορίες.", @@ -554,6 +568,16 @@ "sectionCalendarSync": "Συγχρονισμός ημερολογίου", "sectionFamily": "Μέλη οικογένειας", "cardAppearance": "Εμφάνιση", + "appNameTitle": "Όνομα εφαρμογής", + "appNameLabel": "Όνομα εφαρμογής", + "appNameHint": "Αυτό το όνομα εμφανίζεται στην πλαϊνή μπάρα, στον τίτλο του προγράμματος περιήγησης και στην οθόνη σύνδεσης.", + "appNamePlaceholder": "Oikos", + "appNameSavedToast": "Το όνομα εφαρμογής αποθηκεύτηκε.", + "sectionDate": "Ημερομηνία", + "dateFormatTitle": "Μορφή ημερομηνίας", + "dateFormatLabel": "Προτιμώμενη μορφή ημερομηνίας", + "dateFormatHint": "Επιλέξτε πώς εμφανίζονται οι ημερομηνίες στην εφαρμογή.", + "dateFormatSavedToast": "Η μορφή ημερομηνίας αποθηκεύτηκε.", "themeSystem": "Σύστημα", "themeSysLabel": "Χρήση ρύθμισης συστήματος", "themeLight": "Ανοιχτό", diff --git a/public/locales/en.json b/public/locales/en.json index 2e85007..68aaba8 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -83,6 +83,19 @@ "allDay": "All day", "shoppingMore": "+{{count}} more", "weather": "Weather", + "familyMembers": "Family members", + "participantsAdded": "participants added", + "upcomingBirthdays": "Upcoming birthdays", + "noBirthdays": "No birthdays yet", + "daysLeft": "{{count}} days", + "budgetOverview": "Budget overview", + "monthlyIncome": "Income", + "monthlyExpenses": "Expenses", + "monthlyBalance": "Balance", + "savingsRate": "Savings rate", + "topExpense": "Top expense", + "budgetEntries": "Entries", + "noBudgetData": "No budget data this month.", "customize": "Customize", "customizeTitle": "Customize widgets", "customizeReset": "Reset", @@ -537,6 +550,7 @@ "tabAccount": "Account", "tabsAriaLabel": "Settings sections", "sectionDesign": "Appearance", + "sectionAppName": "Application name", "sectionShopping": "Shopping", "shoppingCategoriesLabel": "Shopping Categories", "shoppingCategoriesHint": "Add, rename, delete or reorder categories.", @@ -554,6 +568,16 @@ "sectionCalendarSync": "Calendar Sync", "sectionFamily": "Family Members", "cardAppearance": "Display", + "appNameTitle": "App name", + "appNameLabel": "Application name", + "appNameHint": "This name appears in the sidebar, browser title and login screen.", + "appNamePlaceholder": "Oikos", + "appNameSavedToast": "Application name saved.", + "sectionDate": "Date", + "dateFormatTitle": "Date format", + "dateFormatLabel": "Preferred date format", + "dateFormatHint": "Choose how dates are displayed throughout the app.", + "dateFormatSavedToast": "Date format saved.", "themeSystem": "System", "themeSysLabel": "Use system setting", "themeLight": "Light", diff --git a/public/locales/es.json b/public/locales/es.json index ba60756..fba9471 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -83,6 +83,19 @@ "allDay": "Todo el día", "shoppingMore": "+{{count}} más", "weather": "Clima", + "familyMembers": "Miembros de la familia", + "participantsAdded": "participantes añadidos", + "upcomingBirthdays": "Próximos cumpleaños", + "noBirthdays": "Aún no hay cumpleaños", + "daysLeft": "{{count}} días", + "budgetOverview": "Resumen del presupuesto", + "monthlyIncome": "Ingresos", + "monthlyExpenses": "Gastos", + "monthlyBalance": "Saldo", + "savingsRate": "Tasa de ahorro", + "topExpense": "Mayor gasto", + "budgetEntries": "Movimientos", + "noBudgetData": "No hay datos de presupuesto este mes.", "customize": "Personalizar", "customizeTitle": "Personalizar widgets", "customizeReset": "Restablecer", @@ -537,6 +550,7 @@ "tabAccount": "Cuenta", "tabsAriaLabel": "Secciones de configuración", "sectionDesign": "Diseño", + "sectionAppName": "Nombre de la aplicación", "sectionShopping": "Compras", "shoppingCategoriesLabel": "Categorías de compra", "shoppingCategoriesHint": "Añade, renombra, elimina u ordena las categorías.", @@ -554,6 +568,16 @@ "sectionCalendarSync": "Sincronización de calendario", "sectionFamily": "Miembros de la familia", "cardAppearance": "Apariencia", + "appNameTitle": "Nombre de la app", + "appNameLabel": "Nombre de la aplicación", + "appNameHint": "Este nombre aparece en la barra lateral, el título del navegador y la pantalla de inicio de sesión.", + "appNamePlaceholder": "Oikos", + "appNameSavedToast": "Nombre de la aplicación guardado.", + "sectionDate": "Fecha", + "dateFormatTitle": "Formato de fecha", + "dateFormatLabel": "Formato de fecha preferido", + "dateFormatHint": "Elige cómo se muestran las fechas en toda la app.", + "dateFormatSavedToast": "Formato de fecha guardado.", "themeSystem": "Sistema", "themeSysLabel": "Usar configuración del sistema", "themeLight": "Claro", diff --git a/public/locales/fr.json b/public/locales/fr.json index bf82d53..c0e0b3f 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -83,6 +83,19 @@ "allDay": "Toute la journée", "shoppingMore": "+{{count}} de plus", "weather": "Météo", + "familyMembers": "Membres de la famille", + "participantsAdded": "participants ajoutés", + "upcomingBirthdays": "Prochains anniversaires", + "noBirthdays": "Aucun anniversaire pour l'instant", + "daysLeft": "{{count}} jours", + "budgetOverview": "Aperçu du budget", + "monthlyIncome": "Revenus", + "monthlyExpenses": "Dépenses", + "monthlyBalance": "Solde", + "savingsRate": "Taux d'épargne", + "topExpense": "Plus grosse dépense", + "budgetEntries": "Écritures", + "noBudgetData": "Aucune donnée de budget ce mois-ci.", "customize": "Personnaliser", "customizeTitle": "Personnaliser les widgets", "customizeReset": "Réinitialiser", @@ -537,6 +550,7 @@ "tabAccount": "Compte", "tabsAriaLabel": "Sections des paramètres", "sectionDesign": "Apparence", + "sectionAppName": "Nom de l'application", "sectionShopping": "Courses", "shoppingCategoriesLabel": "Catégories de courses", "shoppingCategoriesHint": "Ajoutez, renommez, supprimez ou réorganisez les catégories.", @@ -554,6 +568,16 @@ "sectionCalendarSync": "Synchronisation du calendrier", "sectionFamily": "Membres de la famille", "cardAppearance": "Affichage", + "appNameTitle": "Nom de l'application", + "appNameLabel": "Nom de l'application", + "appNameHint": "Ce nom apparaît dans la barre latérale, le titre du navigateur et l'écran de connexion.", + "appNamePlaceholder": "Oikos", + "appNameSavedToast": "Nom de l'application enregistré.", + "sectionDate": "Date", + "dateFormatTitle": "Format de date", + "dateFormatLabel": "Format de date préféré", + "dateFormatHint": "Choisissez comment les dates sont affichées dans l'application.", + "dateFormatSavedToast": "Format de date enregistré.", "themeSystem": "Système", "themeSysLabel": "Utiliser le paramètre système", "themeLight": "Clair", diff --git a/public/locales/hi.json b/public/locales/hi.json index ec68ba9..252117a 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -83,6 +83,19 @@ "allDay": "पूरे दिन", "shoppingMore": "+{{count}} और", "weather": "मौसम", + "familyMembers": "परिवार के सदस्य", + "participantsAdded": "प्रतिभागी जोड़े गए", + "upcomingBirthdays": "आने वाले जन्मदिन", + "noBirthdays": "अभी कोई जन्मदिन नहीं", + "daysLeft": "{{count}} दिन", + "budgetOverview": "बजट अवलोकन", + "monthlyIncome": "आय", + "monthlyExpenses": "खर्च", + "monthlyBalance": "शेष", + "savingsRate": "बचत दर", + "topExpense": "सबसे बड़ा खर्च", + "budgetEntries": "प्रविष्टियां", + "noBudgetData": "इस महीने बजट डेटा नहीं है।", "customize": "अनुकूलित करें", "customizeTitle": "विजेट अनुकूलित करें", "customizeReset": "डिफ़ॉल्ट", @@ -537,6 +550,7 @@ "tabAccount": "खाता", "tabsAriaLabel": "सेटिंग्स अनुभाग", "sectionDesign": "डिज़ाइन", + "sectionAppName": "ऐप का नाम", "sectionShopping": "खरीदारी", "shoppingCategoriesLabel": "खरीदारी श्रेणियां", "shoppingCategoriesHint": "श्रेणियां जोड़ें, नाम बदलें, हटाएं या क्रम बदलें।", @@ -554,6 +568,16 @@ "sectionCalendarSync": "कैलेंडर सिंक", "sectionFamily": "परिवार के सदस्य", "cardAppearance": "दिखावट", + "appNameTitle": "ऐप का नाम", + "appNameLabel": "ऐप का नाम", + "appNameHint": "यह नाम साइडबार, ब्राउज़र शीर्षक और लॉगिन स्क्रीन में दिखाई देगा।", + "appNamePlaceholder": "Oikos", + "appNameSavedToast": "ऐप का नाम सहेजा गया।", + "sectionDate": "तारीख", + "dateFormatTitle": "तारीख प्रारूप", + "dateFormatLabel": "पसंदीदा तारीख प्रारूप", + "dateFormatHint": "चुनें कि ऐप में तारीखें कैसे दिखाई दें।", + "dateFormatSavedToast": "तारीख प्रारूप सहेजा गया।", "themeSystem": "सिस्टम", "themeSysLabel": "सिस्टम सेटिंग का उपयोग करें", "themeLight": "हल्का", diff --git a/public/locales/it.json b/public/locales/it.json index 1d1a2be..a790dfb 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -83,6 +83,19 @@ "allDay": "Tutto il giorno", "shoppingMore": "+{{count}} altri", "weather": "Meteo", + "familyMembers": "Membri della famiglia", + "participantsAdded": "partecipanti aggiunti", + "upcomingBirthdays": "Prossimi compleanni", + "noBirthdays": "Ancora nessun compleanno", + "daysLeft": "{{count}} giorni", + "budgetOverview": "Panoramica budget", + "monthlyIncome": "Entrate", + "monthlyExpenses": "Uscite", + "monthlyBalance": "Saldo", + "savingsRate": "Tasso di risparmio", + "topExpense": "Spesa principale", + "budgetEntries": "Movimenti", + "noBudgetData": "Nessun dato di budget questo mese.", "customize": "Personalizza", "customizeTitle": "Personalizza widget", "customizeReset": "Ripristina", @@ -537,6 +550,7 @@ "tabAccount": "Account", "tabsAriaLabel": "Sezioni impostazioni", "sectionDesign": "Aspetto", + "sectionAppName": "Nome dell'applicazione", "sectionShopping": "Spesa", "shoppingCategoriesLabel": "Categorie spesa", "shoppingCategoriesHint": "Aggiungi, rinomina, elimina o riordina le categorie.", @@ -554,6 +568,16 @@ "sectionCalendarSync": "Sincronizzazione calendario", "sectionFamily": "Membri della famiglia", "cardAppearance": "Visualizzazione", + "appNameTitle": "Nome dell'app", + "appNameLabel": "Nome dell'applicazione", + "appNameHint": "Questo nome appare nella barra laterale, nel titolo del browser e nella schermata di accesso.", + "appNamePlaceholder": "Oikos", + "appNameSavedToast": "Nome dell'applicazione salvato.", + "sectionDate": "Data", + "dateFormatTitle": "Formato data", + "dateFormatLabel": "Formato data preferito", + "dateFormatHint": "Scegli come vengono mostrate le date nell'app.", + "dateFormatSavedToast": "Formato data salvato.", "themeSystem": "Sistema", "themeSysLabel": "Usa impostazione di sistema", "themeLight": "Chiaro", diff --git a/public/locales/ja.json b/public/locales/ja.json index 448e42d..68b4694 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -83,6 +83,19 @@ "allDay": "終日", "shoppingMore": "+{{count}} 件", "weather": "天気", + "familyMembers": "家族メンバー", + "participantsAdded": "人が追加済み", + "upcomingBirthdays": "今後の誕生日", + "noBirthdays": "誕生日はまだありません", + "daysLeft": "{{count}}日", + "budgetOverview": "予算の概要", + "monthlyIncome": "収入", + "monthlyExpenses": "支出", + "monthlyBalance": "残高", + "savingsRate": "貯蓄率", + "topExpense": "最大の支出", + "budgetEntries": "記録", + "noBudgetData": "今月の予算データはありません。", "customize": "カスタマイズ", "customizeTitle": "ウィジェットのカスタマイズ", "customizeReset": "デフォルト", @@ -537,6 +550,7 @@ "tabAccount": "アカウント", "tabsAriaLabel": "設定カテゴリー", "sectionDesign": "デザイン", + "sectionAppName": "アプリ名", "sectionShopping": "買い物", "shoppingCategoriesLabel": "買い物カテゴリー", "shoppingCategoriesHint": "カテゴリーの追加、名前変更、削除、並び替えができます。", @@ -554,6 +568,16 @@ "sectionCalendarSync": "カレンダー同期", "sectionFamily": "家族メンバー", "cardAppearance": "外観", + "appNameTitle": "アプリ名", + "appNameLabel": "アプリ名", + "appNameHint": "この名前はサイドバー、ブラウザのタイトル、ログイン画面に表示されます。", + "appNamePlaceholder": "Oikos", + "appNameSavedToast": "アプリ名を保存しました。", + "sectionDate": "日付", + "dateFormatTitle": "日付形式", + "dateFormatLabel": "希望する日付形式", + "dateFormatHint": "アプリ内で日付をどう表示するかを選択します。", + "dateFormatSavedToast": "日付形式を保存しました。", "themeSystem": "システム設定", "themeSysLabel": "システム設定を使用", "themeLight": "ライト", diff --git a/public/locales/pt.json b/public/locales/pt.json index 161932c..9ee9b9f 100644 --- a/public/locales/pt.json +++ b/public/locales/pt.json @@ -83,6 +83,19 @@ "allDay": "Dia inteiro", "shoppingMore": "+{{count}} mais", "weather": "Clima", + "familyMembers": "Membros da família", + "participantsAdded": "participantes adicionados", + "upcomingBirthdays": "Próximos aniversários", + "noBirthdays": "Nenhum aniversário ainda", + "daysLeft": "{{count}} dias", + "budgetOverview": "Visão do orçamento", + "monthlyIncome": "Receitas", + "monthlyExpenses": "Despesas", + "monthlyBalance": "Saldo", + "savingsRate": "Taxa de poupança", + "topExpense": "Maior despesa", + "budgetEntries": "Lançamentos", + "noBudgetData": "Sem dados de orçamento neste mês.", "customize": "Personalizar", "customizeTitle": "Personalizar widgets", "customizeReset": "Padrão", @@ -537,6 +550,7 @@ "tabAccount": "Conta", "tabsAriaLabel": "Seções de configurações", "sectionDesign": "Design", + "sectionAppName": "Nome da aplicação", "sectionShopping": "Compras", "shoppingCategoriesLabel": "Categorias de compras", "shoppingCategoriesHint": "Adicione, renomeie, exclua ou ordene categorias.", @@ -554,6 +568,16 @@ "sectionCalendarSync": "Sincronização de calendário", "sectionFamily": "Membros da família", "cardAppearance": "Aparência", + "appNameTitle": "Nome do app", + "appNameLabel": "Nome da aplicação", + "appNameHint": "Este nome aparece na barra lateral, no título do navegador e no ecrã de login.", + "appNamePlaceholder": "Oikos", + "appNameSavedToast": "Nome da aplicação guardado.", + "sectionDate": "Data", + "dateFormatTitle": "Formato da data", + "dateFormatLabel": "Formato preferido da data", + "dateFormatHint": "Escolha como as datas aparecem em toda a aplicação.", + "dateFormatSavedToast": "Formato da data salvo.", "themeSystem": "Sistema", "themeSysLabel": "Usar configuração do sistema", "themeLight": "Claro", diff --git a/public/locales/ru.json b/public/locales/ru.json index 9b93928..6e145f1 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -83,6 +83,19 @@ "allDay": "Весь день", "shoppingMore": "+{{count}} ещё", "weather": "Погода", + "familyMembers": "Члены семьи", + "participantsAdded": "участников добавлено", + "upcomingBirthdays": "Ближайшие дни рождения", + "noBirthdays": "Дней рождения пока нет", + "daysLeft": "{{count}} дн.", + "budgetOverview": "Обзор бюджета", + "monthlyIncome": "Доходы", + "monthlyExpenses": "Расходы", + "monthlyBalance": "Баланс", + "savingsRate": "Норма сбережений", + "topExpense": "Крупнейший расход", + "budgetEntries": "Записи", + "noBudgetData": "Нет данных бюджета за этот месяц.", "customize": "Настроить", "customizeTitle": "Настроить виджеты", "customizeReset": "Сбросить", @@ -537,6 +550,7 @@ "tabAccount": "Аккаунт", "tabsAriaLabel": "Разделы настроек", "sectionDesign": "Внешний вид", + "sectionAppName": "Название приложения", "sectionShopping": "Покупки", "shoppingCategoriesLabel": "Категории покупок", "shoppingCategoriesHint": "Добавляйте, переименовывайте, удаляйте или сортируйте категории.", @@ -554,6 +568,16 @@ "sectionCalendarSync": "Синхронизация календаря", "sectionFamily": "Члены семьи", "cardAppearance": "Отображение", + "appNameTitle": "Название приложения", + "appNameLabel": "Название приложения", + "appNameHint": "Это название отображается в боковом меню, заголовке браузера и на экране входа.", + "appNamePlaceholder": "Oikos", + "appNameSavedToast": "Название приложения сохранено.", + "sectionDate": "Дата", + "dateFormatTitle": "Формат даты", + "dateFormatLabel": "Предпочитаемый формат даты", + "dateFormatHint": "Выберите, как даты отображаются в приложении.", + "dateFormatSavedToast": "Формат даты сохранён.", "themeSystem": "Система", "themeSysLabel": "Использовать системную настройку", "themeLight": "Светлая", diff --git a/public/locales/sv.json b/public/locales/sv.json index 3914485..f546000 100644 --- a/public/locales/sv.json +++ b/public/locales/sv.json @@ -83,6 +83,19 @@ "allDay": "Hela dagen", "shoppingMore": "+{{count}} till", "weather": "Väder", + "familyMembers": "Familjemedlemmar", + "participantsAdded": "deltagare tillagda", + "upcomingBirthdays": "Kommande födelsedagar", + "noBirthdays": "Inga födelsedagar ännu", + "daysLeft": "{{count}} dagar", + "budgetOverview": "Budgetöversikt", + "monthlyIncome": "Inkomster", + "monthlyExpenses": "Utgifter", + "monthlyBalance": "Saldo", + "savingsRate": "Sparandegrad", + "topExpense": "Största utgift", + "budgetEntries": "Poster", + "noBudgetData": "Ingen budgetdata denna månad.", "customize": "Anpassa", "customizeTitle": "Anpassa widgets", "customizeReset": "Återställ", @@ -537,6 +550,7 @@ "tabAccount": "Konto", "tabsAriaLabel": "Inställningsavsnitt", "sectionDesign": "Utseende", + "sectionAppName": "Appnamn", "sectionShopping": "Inköp", "shoppingCategoriesLabel": "Inköpskategorier", "shoppingCategoriesHint": "Lägg till, byt namn, ta bort eller sortera om kategorier.", @@ -554,6 +568,16 @@ "sectionCalendarSync": "Kalendersynkronisering", "sectionFamily": "Familjemedlemmar", "cardAppearance": "Visa", + "appNameTitle": "Appnamn", + "appNameLabel": "Appnamn", + "appNameHint": "Det här namnet visas i sidomenyn, webbläsarens titel och inloggningsskärmen.", + "appNamePlaceholder": "Oikos", + "appNameSavedToast": "Appnamn sparat.", + "sectionDate": "Datum", + "dateFormatTitle": "Datumformat", + "dateFormatLabel": "Önskat datumformat", + "dateFormatHint": "Välj hur datum visas i appen.", + "dateFormatSavedToast": "Datumformat sparat.", "themeSystem": "System", "themeSysLabel": "Använd systeminställning", "themeLight": "Ljus", diff --git a/public/locales/tr.json b/public/locales/tr.json index bc78fb8..faadd2d 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -83,6 +83,19 @@ "allDay": "Tüm gün", "shoppingMore": "+{{count}} daha", "weather": "Hava", + "familyMembers": "Aile üyeleri", + "participantsAdded": "katılımcı eklendi", + "upcomingBirthdays": "Yaklaşan doğum günleri", + "noBirthdays": "Henüz doğum günü yok", + "daysLeft": "{{count}} gün", + "budgetOverview": "Bütçe özeti", + "monthlyIncome": "Gelir", + "monthlyExpenses": "Giderler", + "monthlyBalance": "Bakiye", + "savingsRate": "Tasarruf oranı", + "topExpense": "En büyük gider", + "budgetEntries": "Kayıtlar", + "noBudgetData": "Bu ay bütçe verisi yok.", "customize": "Özelleştir", "customizeTitle": "Widget'ları özelleştir", "customizeReset": "Sıfırla", @@ -537,6 +550,7 @@ "tabAccount": "Hesap", "tabsAriaLabel": "Ayar bölümleri", "sectionDesign": "Görünüm", + "sectionAppName": "Uygulama adı", "sectionShopping": "Alışveriş", "shoppingCategoriesLabel": "Alışveriş Kategorileri", "shoppingCategoriesHint": "Kategorileri ekleyin, yeniden adlandırın, silin veya sıralayın.", @@ -554,6 +568,16 @@ "sectionCalendarSync": "Takvim Senkronizasyonu", "sectionFamily": "Aile Üyeleri", "cardAppearance": "Görünüm", + "appNameTitle": "Uygulama adı", + "appNameLabel": "Uygulama adı", + "appNameHint": "Bu ad kenar çubuğunda, tarayıcı başlığında ve giriş ekranında görünür.", + "appNamePlaceholder": "Oikos", + "appNameSavedToast": "Uygulama adı kaydedildi.", + "sectionDate": "Tarih", + "dateFormatTitle": "Tarih biçimi", + "dateFormatLabel": "Tercih edilen tarih biçimi", + "dateFormatHint": "Tarihlerin uygulamada nasıl görüneceğini seçin.", + "dateFormatSavedToast": "Tarih biçimi kaydedildi.", "themeSystem": "Sistem", "themeSysLabel": "Sistem ayarını kullan", "themeLight": "Açık", diff --git a/public/locales/uk.json b/public/locales/uk.json index 1c6bbe2..c035241 100644 --- a/public/locales/uk.json +++ b/public/locales/uk.json @@ -83,6 +83,19 @@ "allDay": "Весь день", "shoppingMore": "+{{count}} ще", "weather": "Погода", + "familyMembers": "Члени родини", + "participantsAdded": "учасників додано", + "upcomingBirthdays": "Найближчі дні народження", + "noBirthdays": "Днів народження ще немає", + "daysLeft": "{{count}} дн.", + "budgetOverview": "Огляд бюджету", + "monthlyIncome": "Доходи", + "monthlyExpenses": "Витрати", + "monthlyBalance": "Баланс", + "savingsRate": "Рівень заощаджень", + "topExpense": "Найбільша витрата", + "budgetEntries": "Записи", + "noBudgetData": "Немає бюджетних даних за цей місяць.", "customize": "Налаштувати", "customizeTitle": "Налаштувати віджети", "customizeReset": "Скинути", @@ -537,6 +550,7 @@ "tabAccount": "Обліковий запис", "tabsAriaLabel": "Розділи налаштувань", "sectionDesign": "Зовнішній вигляд", + "sectionAppName": "Назва застосунку", "sectionShopping": "Покупки", "shoppingCategoriesLabel": "Категорії покупок", "shoppingCategoriesHint": "Додавайте, перейменовуйте, видаляйте або змінюйте порядок категорій.", @@ -554,6 +568,16 @@ "sectionCalendarSync": "Синхронізація календаря", "sectionFamily": "Члени родини", "cardAppearance": "Відображення", + "appNameTitle": "Назва застосунку", + "appNameLabel": "Назва застосунку", + "appNameHint": "Ця назва відображається в бічному меню, заголовку браузера та на екрані входу.", + "appNamePlaceholder": "Oikos", + "appNameSavedToast": "Назву застосунку збережено.", + "sectionDate": "Дата", + "dateFormatTitle": "Формат дати", + "dateFormatLabel": "Бажаний формат дати", + "dateFormatHint": "Виберіть, як дати відображаються в застосунку.", + "dateFormatSavedToast": "Формат дати збережено.", "themeSystem": "Системна", "themeSysLabel": "Використовувати системні налаштування", "themeLight": "Світла", diff --git a/public/locales/zh.json b/public/locales/zh.json index 4de2055..b483620 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -83,6 +83,19 @@ "allDay": "全天", "shoppingMore": "+{{count}} 更多", "weather": "天气", + "familyMembers": "家庭成员", + "participantsAdded": "位参与者已添加", + "upcomingBirthdays": "即将到来的生日", + "noBirthdays": "还没有生日", + "daysLeft": "{{count}} 天", + "budgetOverview": "预算概览", + "monthlyIncome": "收入", + "monthlyExpenses": "支出", + "monthlyBalance": "余额", + "savingsRate": "储蓄率", + "topExpense": "最大支出", + "budgetEntries": "记录", + "noBudgetData": "本月没有预算数据。", "customize": "自定义", "customizeTitle": "自定义小组件", "customizeReset": "重置", @@ -537,6 +550,7 @@ "tabAccount": "账户", "tabsAriaLabel": "设置类别", "sectionDesign": "外观", + "sectionAppName": "应用名称", "sectionShopping": "购物", "shoppingCategoriesLabel": "购物分类", "shoppingCategoriesHint": "添加、重命名、删除或排序分类。", @@ -554,6 +568,16 @@ "sectionCalendarSync": "日历同步", "sectionFamily": "家庭成员", "cardAppearance": "外观", + "appNameTitle": "应用名称", + "appNameLabel": "应用名称", + "appNameHint": "此名称会显示在侧边栏、浏览器标题和登录界面中。", + "appNamePlaceholder": "Oikos", + "appNameSavedToast": "应用名称已保存。", + "sectionDate": "日期", + "dateFormatTitle": "日期格式", + "dateFormatLabel": "首选日期格式", + "dateFormatHint": "选择日期在应用中的显示方式。", + "dateFormatSavedToast": "日期格式已保存。", "themeSystem": "跟随系统", "themeSysLabel": "使用系统设置", "themeLight": "浅色", diff --git a/public/pages/birthdays.js b/public/pages/birthdays.js index 15f31f3..8ec7284 100644 --- a/public/pages/birthdays.js +++ b/public/pages/birthdays.js @@ -35,11 +35,11 @@ function photoAvatar(birthday, extraClass = '') { function filteredBirthdays() { const q = state.query.trim().toLowerCase(); - if (!q) return state.birthdays; - return state.birthdays.filter((birthday) => + const list = !q ? state.birthdays : state.birthdays.filter((birthday) => birthday.name.toLowerCase().includes(q) || (birthday.notes || '').toLowerCase().includes(q) ); + return [...list].sort((a, b) => a.name.localeCompare(b.name)); } function suggestions() { @@ -94,8 +94,15 @@ function renderUpcoming() {
${photoAvatar(birthday)}
-
${esc(birthday.name)}
-
${esc(formatDate(birthday.next_birthday))}
+
+
+
${esc(birthday.name)}
+
${esc(formatDate(birthday.next_birthday))}
+
+
+ ${birthday.days_until === 0 ? esc(t('common.today')) : birthday.days_until === 1 ? esc(t('common.tomorrow')) : esc(`${birthday.days_until}d`)} +
+
${esc(ageNote(birthday))}
@@ -146,33 +153,42 @@ function renderPage() {

${t('birthdays.title')}

- +

${t('birthdays.calendarHint')}

-
-
-

${t('birthdays.upcomingTitle')}

-

${t('birthdays.upcomingHint')}

-
-
-
+
+ -
-
-

${t('birthdays.peopleTitle')}

-

${t('birthdays.peopleHint')}

-
-
-
+
+
+ +
+
+

${t('birthdays.peopleTitle')}

+

${t('birthdays.peopleHint')}

+
+
+
+
-
-
- `; -} - function renderUrgentTasks(tasks) { if (!tasks.length) { return `
@@ -288,6 +275,43 @@ function renderUpcomingEvents(events) {
`; } +function renderUpcomingBirthdays(birthdays) { + if (!birthdays.length) { + return `
+ ${widgetHeader('cake', t('nav.birthdays'), 0, '/birthdays')} +
+ +
${t('dashboard.noBirthdays')}
+
+
`; + } + + const items = birthdays.map((b) => { + const daysLabel = b.days_until === 0 + ? t('common.today') + : b.days_until === 1 + ? t('common.tomorrow') + : t('dashboard.daysLeft', { count: b.days_until }); + return ` +
+
+ ${b.photo_data ? `` : `${esc(initials(b.name))}`} +
+
+
${esc(b.name)}
+
${formatDate(b.next_birthday)} · ${daysLabel}
+
+
${esc(String(b.next_age ?? ''))}
+
+ `; + }).join(''); + + return `
+ ${widgetHeader('cake', t('nav.birthdays'), birthdays.length, '/birthdays')} +
${items}
+
`; +} + function renderTodayMeals(meals) { const MEAL_ORDER = ['breakfast', 'lunch', 'dinner', 'snack']; @@ -334,6 +358,283 @@ function renderPinnedNotes(notes) { `; } +function renderFamilyWidget(users) { + const visible = users.slice(0, 6); + const avatars = visible.map((u) => ` + + ${esc(initials(u.display_name))} + + `).join(''); + + return `
+ ${widgetHeader('users', t('dashboard.familyMembers'), users.length, '/settings')} +
+
${users.length}
+
${t('dashboard.participantsAdded')}
+
${avatars}
+
+
`; +} + +function renderBudgetWidget(budget, currency) { + const income = budget?.income || 0; + const expenses = budget?.expenses || 0; + const balance = budget?.balance || 0; + const savingsRate = income > 0 ? Math.round((balance / income) * 100) : 0; + const balanceTone = balance >= 0 ? 'positive' : 'negative'; + const hasData = (budget?.entryCount || 0) > 0; + + return `
+ ${widgetHeader('wallet', t('dashboard.budgetOverview'), null, '/budget')} +
+
+ ${t('dashboard.monthlyBalance')} + ${formatCurrency(balance, currency)} +
+
+
+ ${t('dashboard.monthlyIncome')} + ${formatCurrency(income, currency)} +
+
+ ${t('dashboard.monthlyExpenses')} + ${formatCurrency(expenses, currency)} +
+
+ ${t('dashboard.savingsRate')} + ${income > 0 ? `${savingsRate}%` : '-'} +
+
+ ${t('dashboard.budgetEntries')} + ${budget?.entryCount || 0} +
+
+ +
+
`; +} + +function renderQuickAction({ route, label, icon, tone = '' }) { + return ` + + `; +} + +function renderKpiTile({ title, value, meta, icon, route, tone = '' }) { + return ` + + `; +} + +function renderDashboardOverview(user, stats = null, weather = null) { + const dateLabel = formatDate(new Date()); + const weatherLabel = weather + ? `${esc(weather.city)} · ${esc(weather.current?.temp)}${weather.units === 'imperial' ? '°F' : weather.units === 'standard' ? 'K' : '°C'}` + : t('dashboard.weather'); + + const actions = [ + { route: '/tasks', label: t('nav.tasks'), icon: 'check-square', tone: 'blue' }, + { route: '/calendar', label: t('nav.calendar'), icon: 'calendar', tone: 'violet' }, + { route: '/shopping', label: t('nav.shopping'), icon: 'shopping-cart', tone: 'green' }, + { route: '/notes', label: t('nav.notes'), icon: 'sticky-note', tone: 'amber' }, + ].map(renderQuickAction).join(''); + + const kpis = stats ? [ + renderKpiTile({ + title: t('tasks.title'), + value: String(stats.overdueCount ?? 0), + meta: t('dashboard.overdue'), + icon: 'alert-circle', + route: '/tasks', + tone: 'danger', + }), + renderKpiTile({ + title: t('nav.calendar'), + value: String(stats.todayEventCount ?? 0), + meta: t('common.today'), + icon: 'calendar-days', + route: '/calendar', + tone: 'calendar', + }), + renderKpiTile({ + title: t('nav.meals'), + value: stats.todayMealTitle ? esc(stats.todayMealTitle) : '-', + meta: t('dashboard.todayMeals'), + icon: 'utensils', + route: '/meals', + tone: 'meals', + }), + renderKpiTile({ + title: t('dashboard.weather'), + value: weatherLabel, + meta: t('dashboard.weatherRefreshTitle'), + icon: 'cloud-sun', + route: '/', + tone: 'weather', + }), + renderKpiTile({ + title: t('nav.birthdays'), + value: String(stats.birthdayCount ?? 0), + meta: t('dashboard.upcomingBirthdays'), + icon: 'cake', + route: '/birthdays', + tone: 'birthdays', + }), + renderKpiTile({ + title: t('dashboard.familyMembers'), + value: String(stats.familyCount ?? 0), + meta: t('dashboard.participantsAdded'), + icon: 'users', + route: '/settings', + tone: 'family', + }), + ].join('') : ` +
+
+
+
+
+
+ `; + + return ` +
+
+
+ ${dateLabel} +

${greeting(user.display_name)}

+
+
+
${actions}
+ +
+
+
+ ${kpis} +
+
+ `; +} + +function widgetRegion(id) { + return ['budget', 'family', 'weather', 'shopping', 'meals'].includes(id) ? 'side' : 'main'; +} + +function widgetTileClass(id) { + const map = { + tasks: 'dashboard-tile--wide', + calendar: 'dashboard-tile--compact', + birthdays: 'dashboard-tile--compact', + budget: 'dashboard-tile--wide', + family: 'dashboard-tile--compact', + meals: 'dashboard-tile--compact', + notes: 'dashboard-tile--wide', + shopping: 'dashboard-tile--compact', + weather: 'dashboard-tile--wide', + }; + return map[id] || 'dashboard-tile--compact'; +} + +function renderDashboardTile(id, html) { + if (!html) return ''; + return `
${html}
`; +} + +function renderDashboardLayout(cfg, data, weather, currency) { + const widgetById = { + tasks: () => renderUrgentTasks(data.urgentTasks ?? []), + calendar: () => renderUpcomingEvents(data.upcomingEvents ?? []), + birthdays: () => renderUpcomingBirthdays(data.birthdays ?? []), + budget: () => renderBudgetWidget(data.budget ?? {}, currency), + family: () => renderFamilyWidget(data.users ?? []), + meals: () => renderTodayMeals(data.todayMeals ?? []), + notes: () => renderPinnedNotes(data.pinnedNotes ?? []), + shopping: () => renderShoppingLists(data.shoppingLists ?? []), + weather: () => (weather ? renderWeatherWidget(weather) : ''), + }; + + const visible = cfg.filter((w) => w.visible && widgetById[w.id]); + const mainTiles = visible + .filter((w) => widgetRegion(w.id) === 'main') + .map((w) => renderDashboardTile(w.id, widgetById[w.id]())) + .join(''); + + const sideTiles = visible + .filter((w) => widgetRegion(w.id) === 'side') + .map((w) => renderDashboardTile(w.id, widgetById[w.id]())) + .join(''); + + return ` +
+
+
+ ${mainTiles} +
+
+ +
+ `; +} + +function renderDashboardSkeleton() { + return ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${skeletonWidget(3)} + ${skeletonWidget(3)} + ${skeletonWidget(2)} + ${skeletonWidget(3)} +
+
+ +
+ `; +} + // -------------------------------------------------------- // Shopping-Widget // -------------------------------------------------------- @@ -515,25 +816,6 @@ function initFab(container, signal) { document.addEventListener('click', () => { if (open) toggleFab(false); }, { signal }); } -// -------------------------------------------------------- -// Widget-Rendering nach Konfiguration -// -------------------------------------------------------- - -function renderWidgets(cfg, data, weather) { - const renderers = { - tasks: () => renderUrgentTasks(data.urgentTasks ?? []), - calendar: () => renderUpcomingEvents(data.upcomingEvents ?? []), - shopping: () => renderShoppingLists(data.shoppingLists ?? []), - meals: () => renderTodayMeals(data.todayMeals ?? []), - notes: () => renderPinnedNotes(data.pinnedNotes ?? []), - weather: () => (weather ? renderWeatherWidget(weather) : ''), - }; - return cfg - .filter((w) => w.visible) - .map((w) => (renderers[w.id] ? renderers[w.id]() : '')) - .join(''); -} - // -------------------------------------------------------- // Customize-Modal // -------------------------------------------------------- @@ -728,20 +1010,17 @@ export async function render(container, { user }) { container.innerHTML = `

${t('dashboard.title')}

-
- ${renderGreeting(user, {})} - ${skeletonWidget(3)} - ${skeletonWidget(3)} - ${skeletonWidget(2)} - ${skeletonWidget(3)} +
+ ${renderDashboardSkeleton()}
${renderFab()} `; - let data = { upcomingEvents: [], urgentTasks: [], todayMeals: [], pinnedNotes: [], shoppingLists: [] }; + let data = { upcomingEvents: [], urgentTasks: [], todayMeals: [], pinnedNotes: [], shoppingLists: [], birthdays: [], users: [], budget: {} }; let weather = null; let widgetConfig = DEFAULT_WIDGET_CONFIG; + let currency = 'EUR'; try { const [dashRes, weatherRes, prefsRes] = await Promise.all([ api.get('/dashboard'), @@ -751,6 +1030,7 @@ export async function render(container, { user }) { data = dashRes; weather = weatherRes.data ?? null; widgetConfig = prefsRes.data?.dashboard_widgets ?? DEFAULT_WIDGET_CONFIG; + currency = prefsRes.data?.currency ?? 'EUR'; } catch (err) { console.error('[Dashboard] Ladefehler:', err.message, 'Status:', err.status ?? 'network'); window.oikos?.showToast(t('dashboard.loadError'), 'warning'); @@ -772,52 +1052,45 @@ export async function render(container, { user }) { todayMealTitle: (data.todayMeals ?? []).find((m) => m.meal_type === 'lunch')?.title ?? (data.todayMeals ?? [])[0]?.title ?? null, + birthdayCount: data.birthdayCount ?? (data.birthdays ?? []).length, + familyCount: (data.users ?? []).length, }; const rerender = () => render(container, { user }); - function rebuildGrid(cfg) { - const grid = container.querySelector('.dashboard__grid'); - if (!grid) return; - const greeting = grid.querySelector('.widget-greeting'); - grid.replaceChildren(...(greeting ? [greeting] : [])); - grid.insertAdjacentHTML('beforeend', renderWidgets(cfg, data, weather)); + function rebuildDashboard(cfg) { + const shell = container.querySelector('#dashboard-shell'); + if (!shell) return; + shell.innerHTML = ` + ${renderDashboardOverview(user, stats, weather)} + ${renderDashboardLayout(cfg, data, weather, currency)} + `; wireLinks(container, rerender); if (window.lucide) window.lucide.createIcons(); - wireWeatherRefresh(container); + wireWeatherRefresh(container, (updatedWeather) => { + weather = updatedWeather; + rebuildDashboard(cfg); + }); + container.querySelector('#dashboard-customize-btn')?.addEventListener('click', () => { + openCustomizeModal(widgetConfig, (newConfig) => { + widgetConfig = newConfig; + rebuildDashboard(widgetConfig); + }); + }, { signal: _fabController.signal }); } - // Greeting in-place aktualisieren (Stats-Chips hinzufügen), kein Gesamt-Reset - const greetingEl = container.querySelector('.widget-greeting'); - if (greetingEl) greetingEl.outerHTML = renderGreeting(user, stats); - - // Skeletons durch echte Widgets ersetzen - rebuildGrid(widgetConfig); + rebuildDashboard(widgetConfig); initFab(container, _fabController.signal); - container.querySelector('#dashboard-customize-btn')?.addEventListener( - 'click', - () => openCustomizeModal(widgetConfig, (newConfig) => { - widgetConfig = newConfig; - rebuildGrid(widgetConfig); - }), - { signal: _fabController.signal }, - ); - // 30-Minuten Auto-Refresh für Wetter const refreshBtn = container.querySelector('#weather-refresh-btn'); if (refreshBtn) { const doAutoRefresh = async () => { try { const res = await api.get('/weather').catch(() => ({ data: null })); - const wWidget = container.querySelector('#weather-widget'); - if (wWidget) { - wWidget.outerHTML = renderWeatherWidget(res.data ?? null); - const newWidget = container.querySelector('#weather-widget'); - if (newWidget && window.lucide) window.lucide.createIcons({ el: newWidget }); - wireWeatherRefresh(container); - } + weather = res.data ?? null; + rebuildDashboard(widgetConfig); } catch { /* silently ignore */ } }; const timerId = setInterval(doAutoRefresh, 30 * 60 * 1000); @@ -825,7 +1098,7 @@ export async function render(container, { user }) { } } -function wireWeatherRefresh(container) { +function wireWeatherRefresh(container, onUpdated = null) { const refreshBtn = container.querySelector('#weather-refresh-btn'); if (!refreshBtn) return; const doWeatherRefresh = async () => { @@ -838,7 +1111,7 @@ function wireWeatherRefresh(container) { wWidget.outerHTML = renderWeatherWidget(res.data ?? null); const newWidget = container.querySelector('#weather-widget'); if (newWidget && window.lucide) window.lucide.createIcons({ el: newWidget }); - wireWeatherRefresh(container); + onUpdated?.(res.data ?? null); window.oikos?.showToast(t('dashboard.weatherUpdated'), 'success', 1500); } } catch { /* silently ignore */ } diff --git a/public/pages/login.js b/public/pages/login.js index 464317e..5e1d479 100644 --- a/public/pages/login.js +++ b/public/pages/login.js @@ -8,16 +8,30 @@ import { auth } from '/api.js'; import { t } from '/i18n.js'; const VERSION_URL = '/api/v1/version'; +const DEFAULT_APP_NAME = 'Oikos'; +const APP_NAME_STORAGE_KEY = 'oikos-app-name'; + +function getStoredAppName() { + return localStorage.getItem(APP_NAME_STORAGE_KEY) || DEFAULT_APP_NAME; +} + +function setAppBranding(appName) { + const name = String(appName || '').trim() || DEFAULT_APP_NAME; + document.title = name; + const titleEl = document.querySelector('.login-hero__title'); + if (titleEl) titleEl.textContent = name; +} /** * Rendert die Login-Seite in den gegebenen Container. * @param {HTMLElement} container */ export async function render(container) { + const storedAppName = getStoredAppName(); container.innerHTML = `