diff --git a/public/locales/ar.json b/public/locales/ar.json index 5b522b4..e0d8e75 100644 --- a/public/locales/ar.json +++ b/public/locales/ar.json @@ -550,6 +550,7 @@ "tabBudget": "الميزانية", "tabShopping": "التسوق", "tabCalendar": "التقويم", + "tabFamily": "إدارة العائلة", "tabAccount": "الحساب", "tabsAriaLabel": "أقسام الإعدادات", "sectionDesign": "التصميم", diff --git a/public/locales/de.json b/public/locales/de.json index f45aa51..f8f294e 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -575,6 +575,7 @@ "tabBudget": "Budget", "tabShopping": "Einkauf", "tabCalendar": "Kalender", + "tabFamily": "Familienverwaltung", "tabAccount": "Konto", "tabsAriaLabel": "Einstellungsbereiche", "sectionDesign": "Design", diff --git a/public/locales/el.json b/public/locales/el.json index 60e1729..fac1d08 100644 --- a/public/locales/el.json +++ b/public/locales/el.json @@ -550,6 +550,7 @@ "tabBudget": "Προϋπολογισμός", "tabShopping": "Αγορές", "tabCalendar": "Ημερολόγιο", + "tabFamily": "Διαχείριση οικογένειας", "tabAccount": "Λογαριασμός", "tabsAriaLabel": "Τμήματα ρυθμίσεων", "sectionDesign": "Εμφάνιση", diff --git a/public/locales/en.json b/public/locales/en.json index 6d0cbf4..9ce1361 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -550,6 +550,7 @@ "tabBudget": "Budget", "tabShopping": "Shopping", "tabCalendar": "Calendar", + "tabFamily": "Family Management", "tabAccount": "Account", "tabsAriaLabel": "Settings sections", "sectionDesign": "Appearance", diff --git a/public/locales/es.json b/public/locales/es.json index e7ff8fb..9db0c30 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -550,6 +550,7 @@ "tabBudget": "Presupuesto", "tabShopping": "Compras", "tabCalendar": "Calendario", + "tabFamily": "Gestión familiar", "tabAccount": "Cuenta", "tabsAriaLabel": "Secciones de configuración", "sectionDesign": "Diseño", diff --git a/public/locales/fr.json b/public/locales/fr.json index 2b1f314..ed7b64b 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -550,6 +550,7 @@ "tabBudget": "Budget", "tabShopping": "Courses", "tabCalendar": "Calendrier", + "tabFamily": "Gestion familiale", "tabAccount": "Compte", "tabsAriaLabel": "Sections des paramètres", "sectionDesign": "Apparence", diff --git a/public/locales/hi.json b/public/locales/hi.json index d17219e..739a38d 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -550,6 +550,7 @@ "tabBudget": "बजट", "tabShopping": "खरीदारी", "tabCalendar": "कैलेंडर", + "tabFamily": "परिवार प्रबंधन", "tabAccount": "खाता", "tabsAriaLabel": "सेटिंग्स अनुभाग", "sectionDesign": "डिज़ाइन", diff --git a/public/locales/it.json b/public/locales/it.json index 46a4d8d..72b6d99 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -550,6 +550,7 @@ "tabBudget": "Budget", "tabShopping": "Spesa", "tabCalendar": "Calendario", + "tabFamily": "Gestione famiglia", "tabAccount": "Account", "tabsAriaLabel": "Sezioni impostazioni", "sectionDesign": "Aspetto", diff --git a/public/locales/ja.json b/public/locales/ja.json index defa202..d010baf 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -550,6 +550,7 @@ "tabBudget": "家計", "tabShopping": "買い物", "tabCalendar": "カレンダー", + "tabFamily": "家族管理", "tabAccount": "アカウント", "tabsAriaLabel": "設定カテゴリー", "sectionDesign": "デザイン", diff --git a/public/locales/pt.json b/public/locales/pt.json index a6db098..02cf202 100644 --- a/public/locales/pt.json +++ b/public/locales/pt.json @@ -550,6 +550,7 @@ "tabBudget": "Orçamento", "tabShopping": "Compras", "tabCalendar": "Calendário", + "tabFamily": "Gestão da família", "tabAccount": "Conta", "tabsAriaLabel": "Seções de configurações", "sectionDesign": "Design", diff --git a/public/locales/ru.json b/public/locales/ru.json index 5e127a9..13f4328 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -550,6 +550,7 @@ "tabBudget": "Бюджет", "tabShopping": "Покупки", "tabCalendar": "Календарь", + "tabFamily": "Управление семьей", "tabAccount": "Аккаунт", "tabsAriaLabel": "Разделы настроек", "sectionDesign": "Внешний вид", diff --git a/public/locales/sv.json b/public/locales/sv.json index 10e8537..eb817d0 100644 --- a/public/locales/sv.json +++ b/public/locales/sv.json @@ -550,6 +550,7 @@ "tabBudget": "Budget", "tabShopping": "Inköp", "tabCalendar": "Kalender", + "tabFamily": "Familjehantering", "tabAccount": "Konto", "tabsAriaLabel": "Inställningsavsnitt", "sectionDesign": "Utseende", diff --git a/public/locales/tr.json b/public/locales/tr.json index 4636a3d..684a8f6 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -550,6 +550,7 @@ "tabBudget": "Bütçe", "tabShopping": "Alışveriş", "tabCalendar": "Takvim", + "tabFamily": "Aile Yönetimi", "tabAccount": "Hesap", "tabsAriaLabel": "Ayar bölümleri", "sectionDesign": "Görünüm", diff --git a/public/locales/uk.json b/public/locales/uk.json index 1ad0be7..6ac10ea 100644 --- a/public/locales/uk.json +++ b/public/locales/uk.json @@ -550,6 +550,7 @@ "tabBudget": "Бюджет", "tabShopping": "Покупки", "tabCalendar": "Календар", + "tabFamily": "Керування родиною", "tabAccount": "Обліковий запис", "tabsAriaLabel": "Розділи налаштувань", "sectionDesign": "Зовнішній вигляд", diff --git a/public/locales/zh.json b/public/locales/zh.json index 7c2a6e2..48bc68d 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -550,6 +550,7 @@ "tabBudget": "预算", "tabShopping": "购物", "tabCalendar": "日历", + "tabFamily": "家庭管理", "tabAccount": "账户", "tabsAriaLabel": "设置类别", "sectionDesign": "外观", diff --git a/public/pages/calendar.js b/public/pages/calendar.js index 14e772c..20dc2c9 100644 --- a/public/pages/calendar.js +++ b/public/pages/calendar.js @@ -60,12 +60,12 @@ const EVENT_COLOR_NAMES = () => ({ }); const EVENT_ICON_ALIASES = { - tooth: 'drill', + drill: 'tooth', }; const EVENT_ICONS = [ { value: 'calendar', label: 'Calendar' }, - { value: 'drill', label: 'Dentist' }, + { value: 'tooth', label: 'Dentist' }, { value: 'alarm-clock', label: 'Alarm' }, { value: 'clock', label: 'Time' }, { value: 'bell', label: 'Reminder' }, @@ -168,6 +168,8 @@ const EVENT_ICONS = [ { value: 'cloud-sun', label: 'Weather' }, ]; +const CUSTOM_EVENT_ICONS = new Set(['tooth']); + const HOUR_HEIGHT = 56; // px pro Stunde in Wochen-/Tagesansicht /** @@ -266,8 +268,48 @@ function eventIconName(icon) { return EVENT_ICONS.some((item) => item.value === normalized) ? normalized : 'calendar'; } +function customEventIconHtml(icon, className) { + if (icon !== 'tooth') return ''; + return ``; +} + function eventIconHtml(icon, className = 'event-icon') { - return ``; + const name = eventIconName(icon); + if (CUSTOM_EVENT_ICONS.has(name)) return customEventIconHtml(name, className); + return ``; +} + +function eventIconElement(icon, className = 'event-icon') { + const name = eventIconName(icon); + if (name === 'tooth') { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('class', `${className} event-icon--custom`); + svg.setAttribute('viewBox', '0 0 24 24'); + svg.setAttribute('fill', 'none'); + svg.setAttribute('stroke', 'currentColor'); + svg.setAttribute('stroke-width', '2'); + svg.setAttribute('stroke-linecap', 'round'); + svg.setAttribute('stroke-linejoin', 'round'); + svg.setAttribute('aria-hidden', 'true'); + + const outline = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + outline.setAttribute('d', 'M8.5 3.5c1.2 0 2.1.5 3.5.5s2.3-.5 3.5-.5c2.4 0 4 1.8 4 4.4 0 2.2-1 4.2-1.7 5.7-.7 1.6-.8 3.1-1.1 4.7-.3 1.7-1.1 3.2-2.4 3.2-1.1 0-1.5-1.1-1.8-2.7-.2-1.2-.4-2.1-.5-2.1s-.3.9-.5 2.1c-.3 1.6-.7 2.7-1.8 2.7-1.3 0-2.1-1.5-2.4-3.2-.3-1.6-.4-3.1-1.1-4.7C5.5 12.1 4.5 10.1 4.5 7.9c0-2.6 1.6-4.4 4-4.4Z'); + + const ridge = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + ridge.setAttribute('d', 'M10 6.2c.7.3 1.3.5 2 .5s1.3-.2 2-.5'); + + svg.append(outline, ridge); + return svg; + } + + const el = document.createElement('i'); + el.className = className; + el.dataset.lucide = name; + el.setAttribute('aria-hidden', 'true'); + return el; } function bindDateInputs(root) { @@ -1086,8 +1128,7 @@ function openEventModal({ mode, event = null, date = null, reminder = null }) { if (iconInput) iconInput.value = nextIcon; if (iconTrigger) { iconTrigger.dataset.icon = nextIcon; - const iconEl = iconTrigger.querySelector('[data-lucide]'); - iconEl?.setAttribute('data-lucide', nextIcon); + iconTrigger.replaceChildren(eventIconElement(nextIcon, 'event-icon-picker__trigger-icon')); } iconGrid?.querySelectorAll('.event-icon-picker__option').forEach((btn) => { const active = btn.dataset.icon === nextIcon; @@ -1158,7 +1199,7 @@ function buildEventModalContent({ mode, event, date, reminder = null }) { aria-checked="${selectedIcon === icon.value ? 'true' : 'false'}" aria-label="${esc(icon.label)}" title="${esc(icon.label)}"> - + ${eventIconHtml(icon.value, 'event-icon-picker__option-icon')} ` ).join(''); @@ -1181,7 +1222,7 @@ function buildEventModalContent({ mode, event, date, reminder = null }) { aria-haspopup="true" aria-expanded="false" aria-label="${t('calendar.iconLabel')}"> - + ${eventIconHtml(selectedIcon, 'event-icon-picker__trigger-icon')}
diff --git a/public/pages/settings.js b/public/pages/settings.js index 8ab5d68..33f391c 100644 --- a/public/pages/settings.js +++ b/public/pages/settings.js @@ -6,7 +6,7 @@ import { api, auth } from '/api.js'; import { openModal, closeModal, confirmModal } from '/components/modal.js'; -import { t, formatDate, formatTime, dateInputPlaceholder, formatDateInput, parseDateInput, isDateInputValid } from '/i18n.js'; +import { t, formatDate, formatTime, dateInputPlaceholder, formatDateInput, parseDateInput, isDateInputValid, getDateFormat } from '/i18n.js'; import { esc } from '/utils/html.js'; import '/components/oikos-locale-picker.js'; @@ -56,8 +56,30 @@ function buildFamilyRoleOptions(selected = 'other') { `).join(''); } +function maskDateInputValue(value) { + const digits = String(value || '').replace(/\D/g, '').slice(0, 8); + if (!digits) return ''; + + if (getDateFormat() === 'ymd') { + return [ + digits.slice(0, 4), + digits.slice(4, 6), + digits.slice(6, 8), + ].filter(Boolean).join('-'); + } + + return [ + digits.slice(0, 2), + digits.slice(2, 4), + digits.slice(4, 8), + ].filter(Boolean).join('/'); +} + function bindSettingsDateInputs(root) { root.querySelectorAll('.js-date-input').forEach((input) => { + input.addEventListener('input', () => { + input.value = maskDateInputValue(input.value); + }); input.addEventListener('blur', () => { const parsed = parseDateInput(input.value); if (parsed) input.value = formatDateInput(parsed); @@ -196,9 +218,15 @@ export async function render(container, { user }) { ? (appleStatus.lastSync ? t('settings.configuredLastSync', { date: formatDateTime(appleStatus.lastSync) }) : t('settings.configured')) : t('settings.notConnected'); + const allowedTabs = [ + 'general', 'meals', 'budget', 'shopping', 'calendar', + ...(user?.role === 'admin' ? ['family'] : []), + 'account', + ]; + const storedTab = sessionStorage.getItem(SETTINGS_TAB_KEY) ?? 'general'; const activeTab = (syncOk || syncErr) ? 'calendar' - : (sessionStorage.getItem(SETTINGS_TAB_KEY) ?? 'general'); + : (allowedTabs.includes(storedTab) ? storedTab : 'general'); const panelHidden = (id) => id === activeTab ? '' : ' hidden'; const btnClass = (id) => `settings-tab-btn${id === activeTab ? ' settings-tab-btn--active' : ''}`; @@ -219,6 +247,7 @@ export async function render(container, { user }) { + ${user?.role === 'admin' ? `` : ''} @@ -476,6 +505,74 @@ export async function render(container, { user }) {
+ ${user?.role === 'admin' ? ` + +
+
+

${t('settings.sectionFamily')}

+
+
    + ${users.map(memberHtml).join('')} +
+ +
+ +
+

${t('settings.newMemberTitle')}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +

${t('settings.memberContactBirthdayHint')}

+
+ +

${t('settings.systemAdminHint')}

+ +
+ + +
+
+
+
+
+ ` : ''} +
@@ -560,69 +657,6 @@ export async function render(container, { user }) {
- -
-

${t('settings.sectionFamily')}

-
- - -
- -
-

${t('settings.newMemberTitle')}

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
- - -

${t('settings.memberContactBirthdayHint')}

-
- -

${t('settings.systemAdminHint')}

- -
- - -
-
-
-
` : ''}
diff --git a/public/styles/calendar.css b/public/styles/calendar.css index d9c3dea..2cddff7 100644 --- a/public/styles/calendar.css +++ b/public/styles/calendar.css @@ -583,7 +583,8 @@ transform: scale(0.98); } -.event-icon-picker__trigger i { +.event-icon-picker__trigger i, +.event-icon-picker__trigger svg { width: 22px; height: 22px; } @@ -629,7 +630,8 @@ background: var(--color-accent-light); } -.event-icon-picker__option i { +.event-icon-picker__option i, +.event-icon-picker__option svg { width: 20px; height: 20px; } diff --git a/public/sw.js b/public/sw.js index 085efff..fcf72ab 100644 --- a/public/sw.js +++ b/public/sw.js @@ -13,10 +13,10 @@ * → bypassCacheUntil (in-memory + Cache API für SW-Restart-Robustheit) */ -const SHELL_CACHE = 'oikos-shell-v60'; -const PAGES_CACHE = 'oikos-pages-v55'; -const LOCALES_CACHE = 'oikos-locales-v6'; -const ASSETS_CACHE = 'oikos-assets-v55'; +const SHELL_CACHE = 'oikos-shell-v61'; +const PAGES_CACHE = 'oikos-pages-v56'; +const LOCALES_CACHE = 'oikos-locales-v7'; +const ASSETS_CACHE = 'oikos-assets-v56'; const BYPASS_CACHE = 'oikos-bypass-flag'; const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, LOCALES_CACHE, ASSETS_CACHE]; diff --git a/server/db-schema-test.js b/server/db-schema-test.js index e539c28..99c29cb 100644 --- a/server/db-schema-test.js +++ b/server/db-schema-test.js @@ -346,6 +346,9 @@ const MIGRATIONS_SQL = { SELECT 1 FROM contacts WHERE contacts.family_user_id = users.id ); `, + 17: ` + UPDATE calendar_events SET icon = 'tooth' WHERE icon = 'drill'; + `, }; export { MIGRATIONS_SQL }; diff --git a/server/db.js b/server/db.js index 27ebd56..a42d3a7 100644 --- a/server/db.js +++ b/server/db.js @@ -768,6 +768,13 @@ const MIGRATIONS = [ ); `, }, + { + version: 24, + description: 'Use tooth icon for dentist calendar events', + up: ` + UPDATE calendar_events SET icon = 'tooth' WHERE icon = 'drill'; + `, + }, ]; /** diff --git a/server/routes/calendar.js b/server/routes/calendar.js index 7a5c9a6..040f2a7 100644 --- a/server/routes/calendar.js +++ b/server/routes/calendar.js @@ -22,7 +22,7 @@ const router = express.Router(); const VALID_SOURCES = ['local', 'google', 'apple', 'ics']; const ICS_COLOR_RE = /^#[0-9a-fA-F]{6}$/; const VALID_EVENT_ICONS = new Set([ - 'calendar', 'drill', 'alarm-clock', 'clock', 'bell', 'map-pin', 'home', + 'calendar', 'tooth', 'drill', 'alarm-clock', 'clock', 'bell', 'map-pin', 'home', 'house', 'building', 'hospital', 'stethoscope', 'syringe', 'pill', 'tablets', 'bandage', 'ambulance', 'heart-pulse', 'activity', 'cross', 'scissors', 'shower-head', 'dumbbell', 'trophy', 'car', 'bus', 'train', @@ -55,7 +55,7 @@ function isAdminUser(req) { function eventIcon(value) { const raw = typeof value === 'string' && value.trim() ? value.trim() : 'calendar'; - const icon = raw === 'tooth' ? 'drill' : raw; + const icon = raw === 'drill' ? 'tooth' : raw; return VALID_EVENT_ICONS.has(icon) ? icon : null; }