diff --git a/public/pages/settings.js b/public/pages/settings.js index 8a3d45d..7d1f6bd 100644 --- a/public/pages/settings.js +++ b/public/pages/settings.js @@ -12,6 +12,8 @@ import '/components/oikos-locale-picker.js'; const SUPPORTED_CURRENCIES = ['AED', 'AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'CZK', 'DKK', 'EUR', 'GBP', 'HUF', 'INR', 'JPY', 'NOK', 'PLN', 'RUB', 'SAR', 'SEK', 'TRY', 'UAH', 'USD']; const SETTINGS_TAB_KEY = 'oikos:settings:tab'; +const APP_NAME_STORAGE_KEY = 'oikos-app-name'; +const DEFAULT_APP_NAME = 'Oikos'; const CATEGORY_I18N = { 'Obst & Gemüse': 'shopping.catFruitVeg', @@ -56,7 +58,7 @@ export async function render(container, { user }) { let users = []; let googleStatus = { configured: false, connected: false, lastSync: null }; let appleStatus = { configured: false, lastSync: null }; - let prefs = { visible_meal_types: ['breakfast', 'lunch', 'dinner', 'snack'], currency: 'EUR' }; + let prefs = { visible_meal_types: ['breakfast', 'lunch', 'dinner', 'snack'], currency: 'EUR', date_format: 'mdy', app_name: DEFAULT_APP_NAME }; let categories = []; let icsSubscriptions = []; let apiTokens = []; @@ -80,6 +82,13 @@ export async function render(container, { user }) { if (apiTokensRes.status === 'fulfilled') apiTokens = apiTokensRes.value.data ?? []; } catch (_) { /* non-critical */ } + if (prefs.date_format) { + try { localStorage.setItem('oikos-date-format', prefs.date_format); } catch (_) {} + } + if (prefs.app_name) { + try { localStorage.setItem(APP_NAME_STORAGE_KEY, prefs.app_name); } catch (_) {} + } + const googleStatusText = googleStatus.connected ? (googleStatus.lastSync ? t('settings.connectedLastSync', { date: formatDateTime(googleStatus.lastSync) }) : t('settings.connected')) : googleStatus.configured ? t('settings.notConnected') : t('settings.notConfigured'); @@ -139,6 +148,48 @@ export async function render(container, { user }) { + ${user?.role === 'admin' ? ` +
+

${t('settings.sectionAppName')}

+
+

${t('settings.appNameTitle')}

+

${t('settings.appNameHint')}

+
+
+ + +
+ +
+ + +
+
+
+
+ ` : ''} + +
+

${t('settings.sectionDate')}

+
+

${t('settings.dateFormatTitle')}

+

${t('settings.dateFormatHint')}

+ + +
+
+

${t('settings.languageTitle')}

@@ -514,6 +565,58 @@ function bindEvents(container, user, categories, icsSubscriptions, apiTokens) { }); } + const dateFormatSelect = container.querySelector('#date-format-select'); + if (dateFormatSelect) { + dateFormatSelect.addEventListener('change', async () => { + try { + await api.put('/preferences', { date_format: dateFormatSelect.value }); + try { localStorage.setItem('oikos-date-format', dateFormatSelect.value); } catch (_) {} + window.dispatchEvent(new CustomEvent('date-format-changed', { detail: { dateFormat: dateFormatSelect.value } })); + window.oikos?.showToast(t('settings.dateFormatSavedToast'), 'success'); + } catch (err) { + window.oikos?.showToast(err.message ?? t('common.errorGeneric'), 'danger'); + } + }); + } + + const appNameForm = container.querySelector('#app-name-form'); + if (appNameForm) { + appNameForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const errorEl = container.querySelector('#app-name-error'); + const input = container.querySelector('#app-name-input'); + errorEl.hidden = true; + const value = input.value.trim(); + try { + await api.put('/preferences', { app_name: value }); + try { + if (value) localStorage.setItem(APP_NAME_STORAGE_KEY, value); + else localStorage.removeItem(APP_NAME_STORAGE_KEY); + } catch (_) {} + input.value = value || DEFAULT_APP_NAME; + window.dispatchEvent(new CustomEvent('app-name-changed', { detail: { appName: value || DEFAULT_APP_NAME } })); + window.oikos?.showToast(t('settings.appNameSavedToast'), 'success'); + } catch (err) { + showError(errorEl, err.message ?? t('common.errorGeneric')); + } + }); + + container.querySelector('#app-name-reset-btn')?.addEventListener('click', async () => { + const errorEl = container.querySelector('#app-name-error'); + const input = container.querySelector('#app-name-input'); + errorEl.hidden = true; + input.value = DEFAULT_APP_NAME; + try { + await api.put('/preferences', { app_name: '' }); + try { localStorage.removeItem(APP_NAME_STORAGE_KEY); } catch (_) {} + window.dispatchEvent(new CustomEvent('app-name-changed', { detail: { appName: DEFAULT_APP_NAME } })); + window.oikos?.showToast(t('settings.appNameSavedToast'), 'success'); + } catch (err) { + showError(errorEl, err.message ?? t('common.errorGeneric')); + } + }); + } + // Passwort ändern const passwordForm = container.querySelector('#password-form'); if (passwordForm) { diff --git a/server/index.js b/server/index.js index 75dceb3..be4759d 100644 --- a/server/index.js +++ b/server/index.js @@ -39,6 +39,7 @@ const logOikos = createLogger('Oikos'); const { version: APP_VERSION } = JSON.parse( readFileSync(new URL('../package.json', import.meta.url), 'utf-8') ); +const DEFAULT_APP_NAME = 'Oikos'; const app = express(); const PORT = process.env.PORT || 3000; @@ -165,7 +166,14 @@ app.use('/api/v1/auth', authRouter); // Versionsinformation - keine Authentifizierung erforderlich (Login-Seite benötigt diese) app.get('/api/v1/version', (req, res) => { - res.json({ version: APP_VERSION }); + let appName = DEFAULT_APP_NAME; + try { + const row = db.get().prepare('SELECT value FROM sync_config WHERE key = ?').get('app_name'); + if (row?.value) appName = row.value; + } catch { + // fall back to default + } + res.json({ version: APP_VERSION, app_name: appName }); }); function sendOpenApi(req, res) {