Refine family settings and calendar dentist icon

This commit is contained in:
Rafael Foster
2026-04-28 20:28:50 -03:00
parent 7b85db9b07
commit 69897666fb
22 changed files with 182 additions and 80 deletions
+1
View File
@@ -550,6 +550,7 @@
"tabBudget": "الميزانية", "tabBudget": "الميزانية",
"tabShopping": "التسوق", "tabShopping": "التسوق",
"tabCalendar": "التقويم", "tabCalendar": "التقويم",
"tabFamily": "إدارة العائلة",
"tabAccount": "الحساب", "tabAccount": "الحساب",
"tabsAriaLabel": "أقسام الإعدادات", "tabsAriaLabel": "أقسام الإعدادات",
"sectionDesign": "التصميم", "sectionDesign": "التصميم",
+1
View File
@@ -575,6 +575,7 @@
"tabBudget": "Budget", "tabBudget": "Budget",
"tabShopping": "Einkauf", "tabShopping": "Einkauf",
"tabCalendar": "Kalender", "tabCalendar": "Kalender",
"tabFamily": "Familienverwaltung",
"tabAccount": "Konto", "tabAccount": "Konto",
"tabsAriaLabel": "Einstellungsbereiche", "tabsAriaLabel": "Einstellungsbereiche",
"sectionDesign": "Design", "sectionDesign": "Design",
+1
View File
@@ -550,6 +550,7 @@
"tabBudget": "Προϋπολογισμός", "tabBudget": "Προϋπολογισμός",
"tabShopping": "Αγορές", "tabShopping": "Αγορές",
"tabCalendar": "Ημερολόγιο", "tabCalendar": "Ημερολόγιο",
"tabFamily": "Διαχείριση οικογένειας",
"tabAccount": "Λογαριασμός", "tabAccount": "Λογαριασμός",
"tabsAriaLabel": "Τμήματα ρυθμίσεων", "tabsAriaLabel": "Τμήματα ρυθμίσεων",
"sectionDesign": "Εμφάνιση", "sectionDesign": "Εμφάνιση",
+1
View File
@@ -550,6 +550,7 @@
"tabBudget": "Budget", "tabBudget": "Budget",
"tabShopping": "Shopping", "tabShopping": "Shopping",
"tabCalendar": "Calendar", "tabCalendar": "Calendar",
"tabFamily": "Family Management",
"tabAccount": "Account", "tabAccount": "Account",
"tabsAriaLabel": "Settings sections", "tabsAriaLabel": "Settings sections",
"sectionDesign": "Appearance", "sectionDesign": "Appearance",
+1
View File
@@ -550,6 +550,7 @@
"tabBudget": "Presupuesto", "tabBudget": "Presupuesto",
"tabShopping": "Compras", "tabShopping": "Compras",
"tabCalendar": "Calendario", "tabCalendar": "Calendario",
"tabFamily": "Gestión familiar",
"tabAccount": "Cuenta", "tabAccount": "Cuenta",
"tabsAriaLabel": "Secciones de configuración", "tabsAriaLabel": "Secciones de configuración",
"sectionDesign": "Diseño", "sectionDesign": "Diseño",
+1
View File
@@ -550,6 +550,7 @@
"tabBudget": "Budget", "tabBudget": "Budget",
"tabShopping": "Courses", "tabShopping": "Courses",
"tabCalendar": "Calendrier", "tabCalendar": "Calendrier",
"tabFamily": "Gestion familiale",
"tabAccount": "Compte", "tabAccount": "Compte",
"tabsAriaLabel": "Sections des paramètres", "tabsAriaLabel": "Sections des paramètres",
"sectionDesign": "Apparence", "sectionDesign": "Apparence",
+1
View File
@@ -550,6 +550,7 @@
"tabBudget": "बजट", "tabBudget": "बजट",
"tabShopping": "खरीदारी", "tabShopping": "खरीदारी",
"tabCalendar": "कैलेंडर", "tabCalendar": "कैलेंडर",
"tabFamily": "परिवार प्रबंधन",
"tabAccount": "खाता", "tabAccount": "खाता",
"tabsAriaLabel": "सेटिंग्स अनुभाग", "tabsAriaLabel": "सेटिंग्स अनुभाग",
"sectionDesign": "डिज़ाइन", "sectionDesign": "डिज़ाइन",
+1
View File
@@ -550,6 +550,7 @@
"tabBudget": "Budget", "tabBudget": "Budget",
"tabShopping": "Spesa", "tabShopping": "Spesa",
"tabCalendar": "Calendario", "tabCalendar": "Calendario",
"tabFamily": "Gestione famiglia",
"tabAccount": "Account", "tabAccount": "Account",
"tabsAriaLabel": "Sezioni impostazioni", "tabsAriaLabel": "Sezioni impostazioni",
"sectionDesign": "Aspetto", "sectionDesign": "Aspetto",
+1
View File
@@ -550,6 +550,7 @@
"tabBudget": "家計", "tabBudget": "家計",
"tabShopping": "買い物", "tabShopping": "買い物",
"tabCalendar": "カレンダー", "tabCalendar": "カレンダー",
"tabFamily": "家族管理",
"tabAccount": "アカウント", "tabAccount": "アカウント",
"tabsAriaLabel": "設定カテゴリー", "tabsAriaLabel": "設定カテゴリー",
"sectionDesign": "デザイン", "sectionDesign": "デザイン",
+1
View File
@@ -550,6 +550,7 @@
"tabBudget": "Orçamento", "tabBudget": "Orçamento",
"tabShopping": "Compras", "tabShopping": "Compras",
"tabCalendar": "Calendário", "tabCalendar": "Calendário",
"tabFamily": "Gestão da família",
"tabAccount": "Conta", "tabAccount": "Conta",
"tabsAriaLabel": "Seções de configurações", "tabsAriaLabel": "Seções de configurações",
"sectionDesign": "Design", "sectionDesign": "Design",
+1
View File
@@ -550,6 +550,7 @@
"tabBudget": "Бюджет", "tabBudget": "Бюджет",
"tabShopping": "Покупки", "tabShopping": "Покупки",
"tabCalendar": "Календарь", "tabCalendar": "Календарь",
"tabFamily": "Управление семьей",
"tabAccount": "Аккаунт", "tabAccount": "Аккаунт",
"tabsAriaLabel": "Разделы настроек", "tabsAriaLabel": "Разделы настроек",
"sectionDesign": "Внешний вид", "sectionDesign": "Внешний вид",
+1
View File
@@ -550,6 +550,7 @@
"tabBudget": "Budget", "tabBudget": "Budget",
"tabShopping": "Inköp", "tabShopping": "Inköp",
"tabCalendar": "Kalender", "tabCalendar": "Kalender",
"tabFamily": "Familjehantering",
"tabAccount": "Konto", "tabAccount": "Konto",
"tabsAriaLabel": "Inställningsavsnitt", "tabsAriaLabel": "Inställningsavsnitt",
"sectionDesign": "Utseende", "sectionDesign": "Utseende",
+1
View File
@@ -550,6 +550,7 @@
"tabBudget": "Bütçe", "tabBudget": "Bütçe",
"tabShopping": "Alışveriş", "tabShopping": "Alışveriş",
"tabCalendar": "Takvim", "tabCalendar": "Takvim",
"tabFamily": "Aile Yönetimi",
"tabAccount": "Hesap", "tabAccount": "Hesap",
"tabsAriaLabel": "Ayar bölümleri", "tabsAriaLabel": "Ayar bölümleri",
"sectionDesign": "Görünüm", "sectionDesign": "Görünüm",
+1
View File
@@ -550,6 +550,7 @@
"tabBudget": "Бюджет", "tabBudget": "Бюджет",
"tabShopping": "Покупки", "tabShopping": "Покупки",
"tabCalendar": "Календар", "tabCalendar": "Календар",
"tabFamily": "Керування родиною",
"tabAccount": "Обліковий запис", "tabAccount": "Обліковий запис",
"tabsAriaLabel": "Розділи налаштувань", "tabsAriaLabel": "Розділи налаштувань",
"sectionDesign": "Зовнішній вигляд", "sectionDesign": "Зовнішній вигляд",
+1
View File
@@ -550,6 +550,7 @@
"tabBudget": "预算", "tabBudget": "预算",
"tabShopping": "购物", "tabShopping": "购物",
"tabCalendar": "日历", "tabCalendar": "日历",
"tabFamily": "家庭管理",
"tabAccount": "账户", "tabAccount": "账户",
"tabsAriaLabel": "设置类别", "tabsAriaLabel": "设置类别",
"sectionDesign": "外观", "sectionDesign": "外观",
+48 -7
View File
@@ -60,12 +60,12 @@ const EVENT_COLOR_NAMES = () => ({
}); });
const EVENT_ICON_ALIASES = { const EVENT_ICON_ALIASES = {
tooth: 'drill', drill: 'tooth',
}; };
const EVENT_ICONS = [ const EVENT_ICONS = [
{ value: 'calendar', label: 'Calendar' }, { value: 'calendar', label: 'Calendar' },
{ value: 'drill', label: 'Dentist' }, { value: 'tooth', label: 'Dentist' },
{ value: 'alarm-clock', label: 'Alarm' }, { value: 'alarm-clock', label: 'Alarm' },
{ value: 'clock', label: 'Time' }, { value: 'clock', label: 'Time' },
{ value: 'bell', label: 'Reminder' }, { value: 'bell', label: 'Reminder' },
@@ -168,6 +168,8 @@ const EVENT_ICONS = [
{ value: 'cloud-sun', label: 'Weather' }, { value: 'cloud-sun', label: 'Weather' },
]; ];
const CUSTOM_EVENT_ICONS = new Set(['tooth']);
const HOUR_HEIGHT = 56; // px pro Stunde in Wochen-/Tagesansicht 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'; return EVENT_ICONS.some((item) => item.value === normalized) ? normalized : 'calendar';
} }
function customEventIconHtml(icon, className) {
if (icon !== 'tooth') return '';
return `<svg class="${className} event-icon--custom" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path 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"/>
<path d="M10 6.2c.7.3 1.3.5 2 .5s1.3-.2 2-.5"/>
</svg>`;
}
function eventIconHtml(icon, className = 'event-icon') { function eventIconHtml(icon, className = 'event-icon') {
return `<i class="${className}" data-lucide="${eventIconName(icon)}" aria-hidden="true"></i>`; const name = eventIconName(icon);
if (CUSTOM_EVENT_ICONS.has(name)) return customEventIconHtml(name, className);
return `<i class="${className}" data-lucide="${name}" aria-hidden="true"></i>`;
}
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) { function bindDateInputs(root) {
@@ -1086,8 +1128,7 @@ function openEventModal({ mode, event = null, date = null, reminder = null }) {
if (iconInput) iconInput.value = nextIcon; if (iconInput) iconInput.value = nextIcon;
if (iconTrigger) { if (iconTrigger) {
iconTrigger.dataset.icon = nextIcon; iconTrigger.dataset.icon = nextIcon;
const iconEl = iconTrigger.querySelector('[data-lucide]'); iconTrigger.replaceChildren(eventIconElement(nextIcon, 'event-icon-picker__trigger-icon'));
iconEl?.setAttribute('data-lucide', nextIcon);
} }
iconGrid?.querySelectorAll('.event-icon-picker__option').forEach((btn) => { iconGrid?.querySelectorAll('.event-icon-picker__option').forEach((btn) => {
const active = btn.dataset.icon === nextIcon; 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-checked="${selectedIcon === icon.value ? 'true' : 'false'}"
aria-label="${esc(icon.label)}" aria-label="${esc(icon.label)}"
title="${esc(icon.label)}"> title="${esc(icon.label)}">
<i data-lucide="${icon.value}" aria-hidden="true"></i> ${eventIconHtml(icon.value, 'event-icon-picker__option-icon')}
</button>` </button>`
).join(''); ).join('');
@@ -1181,7 +1222,7 @@ function buildEventModalContent({ mode, event, date, reminder = null }) {
aria-haspopup="true" aria-haspopup="true"
aria-expanded="false" aria-expanded="false"
aria-label="${t('calendar.iconLabel')}"> aria-label="${t('calendar.iconLabel')}">
<i data-lucide="${selectedIcon}" aria-hidden="true"></i> ${eventIconHtml(selectedIcon, 'event-icon-picker__trigger-icon')}
</button> </button>
</div> </div>
<div class="form-group event-title-picker__title"> <div class="form-group event-title-picker__title">
+99 -65
View File
@@ -6,7 +6,7 @@
import { api, auth } from '/api.js'; import { api, auth } from '/api.js';
import { openModal, closeModal, confirmModal } from '/components/modal.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 { esc } from '/utils/html.js';
import '/components/oikos-locale-picker.js'; import '/components/oikos-locale-picker.js';
@@ -56,8 +56,30 @@ function buildFamilyRoleOptions(selected = 'other') {
`).join(''); `).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) { function bindSettingsDateInputs(root) {
root.querySelectorAll('.js-date-input').forEach((input) => { root.querySelectorAll('.js-date-input').forEach((input) => {
input.addEventListener('input', () => {
input.value = maskDateInputValue(input.value);
});
input.addEventListener('blur', () => { input.addEventListener('blur', () => {
const parsed = parseDateInput(input.value); const parsed = parseDateInput(input.value);
if (parsed) input.value = formatDateInput(parsed); 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')) ? (appleStatus.lastSync ? t('settings.configuredLastSync', { date: formatDateTime(appleStatus.lastSync) }) : t('settings.configured'))
: t('settings.notConnected'); : 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) const activeTab = (syncOk || syncErr)
? 'calendar' ? 'calendar'
: (sessionStorage.getItem(SETTINGS_TAB_KEY) ?? 'general'); : (allowedTabs.includes(storedTab) ? storedTab : 'general');
const panelHidden = (id) => id === activeTab ? '' : ' hidden'; const panelHidden = (id) => id === activeTab ? '' : ' hidden';
const btnClass = (id) => `settings-tab-btn${id === activeTab ? ' settings-tab-btn--active' : ''}`; const btnClass = (id) => `settings-tab-btn${id === activeTab ? ' settings-tab-btn--active' : ''}`;
@@ -219,6 +247,7 @@ export async function render(container, { user }) {
<button class="${btnClass('budget')}" role="tab" data-tab="budget" aria-selected="${btnAria('budget')}">${t('settings.tabBudget')}</button> <button class="${btnClass('budget')}" role="tab" data-tab="budget" aria-selected="${btnAria('budget')}">${t('settings.tabBudget')}</button>
<button class="${btnClass('shopping')}" role="tab" data-tab="shopping" aria-selected="${btnAria('shopping')}">${t('settings.tabShopping')}</button> <button class="${btnClass('shopping')}" role="tab" data-tab="shopping" aria-selected="${btnAria('shopping')}">${t('settings.tabShopping')}</button>
<button class="${btnClass('calendar')}" role="tab" data-tab="calendar" aria-selected="${btnAria('calendar')}">${t('settings.tabCalendar')}</button> <button class="${btnClass('calendar')}" role="tab" data-tab="calendar" aria-selected="${btnAria('calendar')}">${t('settings.tabCalendar')}</button>
${user?.role === 'admin' ? `<button class="${btnClass('family')}" role="tab" data-tab="family" aria-selected="${btnAria('family')}">${t('settings.tabFamily')}</button>` : ''}
<button class="${btnClass('account')}" role="tab" data-tab="account" aria-selected="${btnAria('account')}">${t('settings.tabAccount')}</button> <button class="${btnClass('account')}" role="tab" data-tab="account" aria-selected="${btnAria('account')}">${t('settings.tabAccount')}</button>
</nav> </nav>
@@ -476,6 +505,74 @@ export async function render(container, { user }) {
</section> </section>
</div> </div>
${user?.role === 'admin' ? `
<!-- Panel: Family Management -->
<div class="settings-tab-panel" data-panel="family" role="tabpanel"${panelHidden('family')}>
<section class="settings-section">
<h2 class="settings-section__title">${t('settings.sectionFamily')}</h2>
<div class="settings-card" id="members-card">
<ul class="settings-members" id="members-list">
${users.map(memberHtml).join('')}
</ul>
<button class="btn btn--primary settings-add-btn" id="add-member-btn">${t('settings.addMember')}</button>
</div>
<div class="settings-card settings-card--hidden" id="add-member-form-card">
<h3 class="settings-card__title">${t('settings.newMemberTitle')}</h3>
<form id="add-member-form" class="settings-form">
<div class="form-group">
<label class="form-label" for="new-username">${t('settings.usernameLabel')}</label>
<input class="form-input" type="text" id="new-username" required autocomplete="off" />
</div>
<div class="form-group">
<label class="form-label" for="new-display-name">${t('settings.displayNameLabel')}</label>
<input class="form-input" type="text" id="new-display-name" required />
</div>
<div class="form-group">
<label class="form-label" for="new-member-password">${t('settings.memberPasswordLabel')}</label>
<input class="form-input" type="password" id="new-member-password" minlength="8" required autocomplete="new-password" />
</div>
<div class="form-group">
<label class="form-label" for="new-avatar-color">${t('settings.colorLabel')}</label>
<input class="form-input form-input--color" type="color" id="new-avatar-color" value="#007AFF" />
</div>
<div class="form-group">
<label class="form-label" for="new-family-role">${t('settings.familyRoleLabel')}</label>
<select class="form-input" id="new-family-role">
${buildFamilyRoleOptions()}
</select>
</div>
<div class="modal-grid modal-grid--2">
<div class="form-group">
<label class="form-label" for="new-member-phone">${t('settings.memberPhoneLabel')}</label>
<input class="form-input" type="tel" id="new-member-phone" autocomplete="tel" />
</div>
<div class="form-group">
<label class="form-label" for="new-member-email">${t('settings.memberEmailLabel')}</label>
<input class="form-input" type="email" id="new-member-email" autocomplete="email" />
</div>
</div>
<div class="form-group">
<label class="form-label" for="new-member-birth-date">${t('settings.memberBirthDateLabel')}</label>
<input class="form-input js-date-input" type="text" id="new-member-birth-date" placeholder="${dateInputPlaceholder()}" inputmode="numeric" />
<p class="form-hint">${t('settings.memberContactBirthdayHint')}</p>
</div>
<label class="toggle-row">
<input type="checkbox" id="new-system-admin" />
<span>${t('settings.systemAdminLabel')}</span>
</label>
<p class="form-hint">${t('settings.systemAdminHint')}</p>
<div id="member-error" class="form-error" hidden></div>
<div class="settings-form-actions">
<button type="submit" class="btn btn--primary">${t('settings.createMember')}</button>
<button type="button" class="btn btn--secondary" id="cancel-add-member">${t('settings.cancelAddMember')}</button>
</div>
</form>
</div>
</section>
</div>
` : ''}
<!-- Panel: Konto --> <!-- Panel: Konto -->
<div class="settings-tab-panel" data-panel="account" role="tabpanel"${panelHidden('account')}> <div class="settings-tab-panel" data-panel="account" role="tabpanel"${panelHidden('account')}>
<section class="settings-section"> <section class="settings-section">
@@ -560,69 +657,6 @@ export async function render(container, { user }) {
</form> </form>
</div> </div>
</section> </section>
<section class="settings-section">
<h2 class="settings-section__title">${t('settings.sectionFamily')}</h2>
<div class="settings-card" id="members-card">
<ul class="settings-members" id="members-list">
${users.map(memberHtml).join('')}
</ul>
<button class="btn btn--primary settings-add-btn" id="add-member-btn">${t('settings.addMember')}</button>
</div>
<div class="settings-card settings-card--hidden" id="add-member-form-card">
<h3 class="settings-card__title">${t('settings.newMemberTitle')}</h3>
<form id="add-member-form" class="settings-form">
<div class="form-group">
<label class="form-label" for="new-username">${t('settings.usernameLabel')}</label>
<input class="form-input" type="text" id="new-username" required autocomplete="off" />
</div>
<div class="form-group">
<label class="form-label" for="new-display-name">${t('settings.displayNameLabel')}</label>
<input class="form-input" type="text" id="new-display-name" required />
</div>
<div class="form-group">
<label class="form-label" for="new-member-password">${t('settings.memberPasswordLabel')}</label>
<input class="form-input" type="password" id="new-member-password" minlength="8" required autocomplete="new-password" />
</div>
<div class="form-group">
<label class="form-label" for="new-avatar-color">${t('settings.colorLabel')}</label>
<input class="form-input form-input--color" type="color" id="new-avatar-color" value="#007AFF" />
</div>
<div class="form-group">
<label class="form-label" for="new-family-role">${t('settings.familyRoleLabel')}</label>
<select class="form-input" id="new-family-role">
${buildFamilyRoleOptions()}
</select>
</div>
<div class="modal-grid modal-grid--2">
<div class="form-group">
<label class="form-label" for="new-member-phone">${t('settings.memberPhoneLabel')}</label>
<input class="form-input" type="tel" id="new-member-phone" autocomplete="tel" />
</div>
<div class="form-group">
<label class="form-label" for="new-member-email">${t('settings.memberEmailLabel')}</label>
<input class="form-input" type="email" id="new-member-email" autocomplete="email" />
</div>
</div>
<div class="form-group">
<label class="form-label" for="new-member-birth-date">${t('settings.memberBirthDateLabel')}</label>
<input class="form-input js-date-input" type="text" id="new-member-birth-date" placeholder="${dateInputPlaceholder()}" inputmode="numeric" />
<p class="form-hint">${t('settings.memberContactBirthdayHint')}</p>
</div>
<label class="toggle-row">
<input type="checkbox" id="new-system-admin" />
<span>${t('settings.systemAdminLabel')}</span>
</label>
<p class="form-hint">${t('settings.systemAdminHint')}</p>
<div id="member-error" class="form-error" hidden></div>
<div class="settings-form-actions">
<button type="submit" class="btn btn--primary">${t('settings.createMember')}</button>
<button type="button" class="btn btn--secondary" id="cancel-add-member">${t('settings.cancelAddMember')}</button>
</div>
</form>
</div>
</section>
` : ''} ` : ''}
<section class="settings-section"> <section class="settings-section">
+4 -2
View File
@@ -583,7 +583,8 @@
transform: scale(0.98); transform: scale(0.98);
} }
.event-icon-picker__trigger i { .event-icon-picker__trigger i,
.event-icon-picker__trigger svg {
width: 22px; width: 22px;
height: 22px; height: 22px;
} }
@@ -629,7 +630,8 @@
background: var(--color-accent-light); background: var(--color-accent-light);
} }
.event-icon-picker__option i { .event-icon-picker__option i,
.event-icon-picker__option svg {
width: 20px; width: 20px;
height: 20px; height: 20px;
} }
+4 -4
View File
@@ -13,10 +13,10 @@
* → bypassCacheUntil (in-memory + Cache API für SW-Restart-Robustheit) * → bypassCacheUntil (in-memory + Cache API für SW-Restart-Robustheit)
*/ */
const SHELL_CACHE = 'oikos-shell-v60'; const SHELL_CACHE = 'oikos-shell-v61';
const PAGES_CACHE = 'oikos-pages-v55'; const PAGES_CACHE = 'oikos-pages-v56';
const LOCALES_CACHE = 'oikos-locales-v6'; const LOCALES_CACHE = 'oikos-locales-v7';
const ASSETS_CACHE = 'oikos-assets-v55'; const ASSETS_CACHE = 'oikos-assets-v56';
const BYPASS_CACHE = 'oikos-bypass-flag'; const BYPASS_CACHE = 'oikos-bypass-flag';
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, LOCALES_CACHE, ASSETS_CACHE]; const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, LOCALES_CACHE, ASSETS_CACHE];
+3
View File
@@ -346,6 +346,9 @@ const MIGRATIONS_SQL = {
SELECT 1 FROM contacts WHERE contacts.family_user_id = users.id SELECT 1 FROM contacts WHERE contacts.family_user_id = users.id
); );
`, `,
17: `
UPDATE calendar_events SET icon = 'tooth' WHERE icon = 'drill';
`,
}; };
export { MIGRATIONS_SQL }; export { MIGRATIONS_SQL };
+7
View File
@@ -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';
`,
},
]; ];
/** /**
+2 -2
View File
@@ -22,7 +22,7 @@ const router = express.Router();
const VALID_SOURCES = ['local', 'google', 'apple', 'ics']; const VALID_SOURCES = ['local', 'google', 'apple', 'ics'];
const ICS_COLOR_RE = /^#[0-9a-fA-F]{6}$/; const ICS_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
const VALID_EVENT_ICONS = new Set([ 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', 'house', 'building', 'hospital', 'stethoscope', 'syringe', 'pill',
'tablets', 'bandage', 'ambulance', 'heart-pulse', 'activity', 'cross', 'tablets', 'bandage', 'ambulance', 'heart-pulse', 'activity', 'cross',
'scissors', 'shower-head', 'dumbbell', 'trophy', 'car', 'bus', 'train', 'scissors', 'shower-head', 'dumbbell', 'trophy', 'car', 'bus', 'train',
@@ -55,7 +55,7 @@ function isAdminUser(req) {
function eventIcon(value) { function eventIcon(value) {
const raw = typeof value === 'string' && value.trim() ? value.trim() : 'calendar'; 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; return VALID_EVENT_ICONS.has(icon) ? icon : null;
} }