Einstellungen: Vollbild-Seite, Kontrast, Stundenhöhe, KW-Anzeige
- Einstellungen von Modal-Popup auf Vollbild-Seite mit Seitennavigation umgestellt - Schriftkontrast (4 Stufen) und Linienkontrast (4 Stufen) pro Benutzer gespeichert - Stundenhöhe (40/60/80/100px) in Wochen-/Tagesansicht per Einstellung steuerbar - Kalenderwoche in Monats- und Wochenansicht grösser dargestellt - CSS-Variable --hour-h für dynamische Zeitraster-Höhe in week.js und app.css - Backend: neue Felder text_contrast, line_contrast, hour_height in UserSettings
This commit is contained in:
@@ -132,14 +132,16 @@ function renderView() {
|
||||
},
|
||||
showEventPopup,
|
||||
false,
|
||||
weekStartDay
|
||||
weekStartDay,
|
||||
state.settings.hour_height || 60
|
||||
);
|
||||
} else if (state.currentView === 'day') {
|
||||
renderWeek(container, state.currentDate, evs,
|
||||
(date, switchDay) => { if (!switchDay) openNewEventModal(date); },
|
||||
showEventPopup,
|
||||
true,
|
||||
weekStartDay
|
||||
weekStartDay,
|
||||
state.settings.hour_height || 60
|
||||
);
|
||||
} else {
|
||||
renderAgenda(container, state.currentDate, evs, showEventPopup);
|
||||
@@ -1043,25 +1045,41 @@ function openSettingsModal() {
|
||||
});
|
||||
document.getElementById('cfg-dim-past').checked = !!s.dim_past_events;
|
||||
|
||||
// Show users section only for admins
|
||||
// Set active contrast/hour-height buttons
|
||||
[
|
||||
{ id: 'cfg-text-contrast', val: s.text_contrast || 3 },
|
||||
{ id: 'cfg-line-contrast', val: s.line_contrast || 3 },
|
||||
{ id: 'cfg-hour-height', val: s.hour_height || 60 },
|
||||
].forEach(({ id, val }) => {
|
||||
const sel = document.getElementById(id);
|
||||
if (!sel) return;
|
||||
sel.querySelectorAll('.contrast-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', String(btn.dataset.val) === String(val));
|
||||
});
|
||||
});
|
||||
|
||||
// Show users nav button only for admins
|
||||
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
const usersSection = document.getElementById('settings-users-section');
|
||||
if (user.is_admin) {
|
||||
usersSection.classList.remove('hidden');
|
||||
loadUsers();
|
||||
} else {
|
||||
usersSection.classList.add('hidden');
|
||||
}
|
||||
const usersNavBtn = document.getElementById('settings-nav-users');
|
||||
if (usersNavBtn) usersNavBtn.classList.toggle('hidden', !user.is_admin);
|
||||
if (user.is_admin) loadUsers();
|
||||
|
||||
// Render Google accounts
|
||||
// Activate first panel
|
||||
const firstBtn = document.querySelector('.settings-nav-btn:not(.hidden)');
|
||||
if (firstBtn) activateSettingsPanel(firstBtn.dataset.panel);
|
||||
|
||||
// Render Google accounts and hidden calendars
|
||||
renderGoogleAccounts();
|
||||
|
||||
// Render hidden calendars
|
||||
renderHiddenCalendars();
|
||||
|
||||
openModal('modal-settings');
|
||||
}
|
||||
|
||||
function activateSettingsPanel(panel) {
|
||||
document.querySelectorAll('.settings-nav-btn').forEach(b => b.classList.toggle('active', b.dataset.panel === panel));
|
||||
document.querySelectorAll('.settings-panel').forEach(p => p.classList.toggle('active', p.id === 'settings-panel-' + panel));
|
||||
}
|
||||
|
||||
function renderGoogleAccounts() {
|
||||
const list = document.getElementById('google-accounts-list');
|
||||
if (!list) return;
|
||||
@@ -1203,6 +1221,21 @@ function bindSettingsModal() {
|
||||
});
|
||||
});
|
||||
|
||||
// Panel navigation
|
||||
document.querySelectorAll('.settings-nav-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => activateSettingsPanel(btn.dataset.panel));
|
||||
});
|
||||
|
||||
// Contrast / hour-height selectors
|
||||
document.querySelectorAll('.contrast-selector').forEach(sel => {
|
||||
sel.addEventListener('click', e => {
|
||||
const btn = e.target.closest('.contrast-btn');
|
||||
if (!btn) return;
|
||||
sel.querySelectorAll('.contrast-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('btn-add-user').onclick = () => {
|
||||
document.getElementById('add-user-form').classList.toggle('hidden');
|
||||
};
|
||||
@@ -1223,6 +1256,10 @@ function bindSettingsModal() {
|
||||
};
|
||||
|
||||
document.getElementById('settings-save').onclick = async () => {
|
||||
const getActive = (id) => {
|
||||
const btn = document.querySelector(`#${id} .contrast-btn.active`);
|
||||
return btn ? Number(btn.dataset.val) : null;
|
||||
};
|
||||
const settings = {
|
||||
default_view: document.getElementById('cfg-default-view').value,
|
||||
week_start_day: document.getElementById('cfg-week-start').value,
|
||||
@@ -1230,13 +1267,16 @@ function bindSettingsModal() {
|
||||
accent_color: document.getElementById('cfg-accent-hex').value,
|
||||
today_color: document.getElementById('cfg-today-hex').value,
|
||||
dim_past_events: document.getElementById('cfg-dim-past').checked,
|
||||
text_contrast: getActive('cfg-text-contrast') || 3,
|
||||
line_contrast: getActive('cfg-line-contrast') || 3,
|
||||
hour_height: getActive('cfg-hour-height') || 60,
|
||||
};
|
||||
try {
|
||||
await api.put('/settings/', settings);
|
||||
state.settings = { ...state.settings, ...settings };
|
||||
state.dimPast = settings.dim_past_events;
|
||||
weekStartDay = settings.week_start_day;
|
||||
applyTheme(settings);
|
||||
applyTheme(state.settings);
|
||||
showToast('Einstellungen gespeichert');
|
||||
closeModal('modal-settings');
|
||||
renderMiniCal();
|
||||
|
||||
@@ -63,12 +63,37 @@ export function getISOWeekNumber(d) {
|
||||
return Math.ceil((((date - yearStart) / 86400000) + 1) / 7);
|
||||
}
|
||||
|
||||
const TEXT_CONTRAST = {
|
||||
1: { t1: '#606070', t2: '#484858', t3: '#303040' },
|
||||
2: { t1: '#9090a8', t2: '#6a6a80', t3: '#484860' },
|
||||
3: { t1: '#c8c8d8', t2: '#9090aa', t3: '#55556a' },
|
||||
4: { t1: '#ffffff', t2: '#c0c0d8', t3: '#8888a0' },
|
||||
};
|
||||
const LINE_CONTRAST = {
|
||||
1: { border: '#1e1e2c', light: '#181826' },
|
||||
2: { border: '#2a2a3c', light: '#222230' },
|
||||
3: { border: '#3a3a52', light: '#2e2e40' },
|
||||
4: { border: '#5a5a78', light: '#484860' },
|
||||
};
|
||||
|
||||
export function applyTheme(settings) {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--primary', settings.primary_color || '#4285f4');
|
||||
root.style.setProperty('--primary-dim', hexToRgba(settings.primary_color || '#4285f4', 0.15));
|
||||
root.style.setProperty('--accent', settings.accent_color || '#ea4335');
|
||||
root.style.setProperty('--today-color', settings.today_color || '#4285f4');
|
||||
root.style.setProperty('--primary', settings.primary_color || '#4285f4');
|
||||
root.style.setProperty('--primary-dim', hexToRgba(settings.primary_color || '#4285f4', 0.15));
|
||||
root.style.setProperty('--accent', settings.accent_color || '#ea4335');
|
||||
root.style.setProperty('--today-color', settings.today_color || '#4285f4');
|
||||
|
||||
const tc = TEXT_CONTRAST[settings.text_contrast || 3];
|
||||
root.style.setProperty('--text-1', tc.t1);
|
||||
root.style.setProperty('--text-2', tc.t2);
|
||||
root.style.setProperty('--text-3', tc.t3);
|
||||
|
||||
const lc = LINE_CONTRAST[settings.line_contrast || 3];
|
||||
root.style.setProperty('--border', lc.border);
|
||||
root.style.setProperty('--border-light', lc.light);
|
||||
|
||||
const hh = settings.hour_height || 60;
|
||||
root.style.setProperty('--hour-h', hh + 'px');
|
||||
}
|
||||
|
||||
function hexToRgba(hex, alpha) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { isToday, isPast, dayOfWeek, weekStart, getISOWeekNumber } from '../util
|
||||
|
||||
const DOW_SHORT = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
|
||||
|
||||
export function renderWeek(container, currentDate, events, onSlotClick, onEventClick, isSingleDay = false, weekStartDay = 'monday') {
|
||||
export function renderWeek(container, currentDate, events, onSlotClick, onEventClick, isSingleDay = false, weekStartDay = 'monday', hourH = 60) {
|
||||
// Build the days array (7 days for week, 1 for day)
|
||||
const days = [];
|
||||
if (isSingleDay) {
|
||||
@@ -58,7 +58,6 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
|
||||
).join('');
|
||||
|
||||
// ── Day columns ───────────────────────────────────────
|
||||
// For each day, lay out timed events
|
||||
const dayCols = days.map(day => {
|
||||
const key = dayKey(day);
|
||||
const dayEvs = timedEvs.filter(ev => {
|
||||
@@ -66,18 +65,17 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
|
||||
return isSameDay(s, day);
|
||||
});
|
||||
|
||||
// Compute layout columns for overlapping events
|
||||
const positioned = layoutEvents(dayEvs);
|
||||
|
||||
const hourLines = Array.from({length: 24}, (_, h) =>
|
||||
`<div class="hour-line" style="top:${h * 60}px"><div class="half-line"></div></div>`
|
||||
`<div class="hour-line" style="top:${h * hourH}px"><div class="half-line"></div></div>`
|
||||
).join('');
|
||||
|
||||
const evHtml = positioned.map(({ ev, col, cols }) => {
|
||||
const s = new Date(ev.start);
|
||||
const e = new Date(ev.end);
|
||||
const top = (s.getHours() * 60 + s.getMinutes());
|
||||
const height = Math.max(20, (e - s) / 60000);
|
||||
const top = s.getHours() * hourH + s.getMinutes() * hourH / 60;
|
||||
const height = Math.max(20, (e - s) / 60000 * hourH / 60);
|
||||
const left = (col / cols) * 100;
|
||||
const width = (1 / cols) * 100 - 0.5;
|
||||
const color = ev.color || ev.calendarColor || '#4285f4';
|
||||
@@ -93,7 +91,7 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
return `<div class="week-day-col" data-date="${key}" style="height:${60*24}px">
|
||||
return `<div class="week-day-col" data-date="${key}" style="height:${hourH * 24}px">
|
||||
${hourLines}
|
||||
${evHtml}
|
||||
</div>`;
|
||||
@@ -118,10 +116,10 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
|
||||
|
||||
// Scroll to ~8:00
|
||||
const body = container.querySelector('.week-body');
|
||||
if (body) body.scrollTop = 8 * 60 - 20;
|
||||
if (body) body.scrollTop = 8 * hourH - 20;
|
||||
|
||||
// Render current-time line
|
||||
renderNowLine(container, days);
|
||||
renderNowLine(container, days, hourH);
|
||||
|
||||
// Click: slot
|
||||
container.querySelectorAll('.week-day-col').forEach(col => {
|
||||
@@ -129,9 +127,8 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
|
||||
if (e.target.closest('.timed-event')) return;
|
||||
const rect = col.getBoundingClientRect();
|
||||
const y = e.clientY - rect.top + (container.querySelector('.week-body')?.scrollTop || 0);
|
||||
const mins = Math.floor(y);
|
||||
const h = Math.floor(mins / 60);
|
||||
const m = Math.round((mins % 60) / 15) * 15;
|
||||
const h = Math.floor(y / hourH);
|
||||
const m = Math.round(((y % hourH) / hourH * 60) / 15) * 15;
|
||||
const date = new Date(col.dataset.date + 'T00:00:00');
|
||||
date.setHours(h, m, 0, 0);
|
||||
onSlotClick(date);
|
||||
@@ -164,34 +161,31 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
|
||||
});
|
||||
}
|
||||
|
||||
function renderNowLine(container, days) {
|
||||
function renderNowLine(container, days, hourH = 60) {
|
||||
const now = new Date();
|
||||
const todayCol = container.querySelector(`.week-day-col[data-date="${dayKey(now)}"]`);
|
||||
if (!todayCol) return;
|
||||
|
||||
const top = now.getHours() * 60 + now.getMinutes();
|
||||
const top = now.getHours() * hourH + now.getMinutes() * hourH / 60;
|
||||
const line = document.createElement('div');
|
||||
line.className = 'now-line';
|
||||
line.style.top = top + 'px';
|
||||
line.innerHTML = '<div class="now-dot"></div>';
|
||||
todayCol.appendChild(line);
|
||||
|
||||
// Update every minute
|
||||
setTimeout(() => renderNowLine(container, days), 60000);
|
||||
setTimeout(() => renderNowLine(container, days, hourH), 60000);
|
||||
}
|
||||
|
||||
function layoutEvents(events) {
|
||||
if (!events.length) return [];
|
||||
|
||||
// Sort by start time
|
||||
const sorted = events.slice().sort((a, b) => new Date(a.start) - new Date(b.start));
|
||||
const columns = []; // each column is an array of events
|
||||
const columns = [];
|
||||
|
||||
const result = sorted.map(ev => {
|
||||
const start = new Date(ev.start);
|
||||
const end = new Date(ev.end);
|
||||
|
||||
// Find the first column where the event doesn't overlap
|
||||
let placed = false;
|
||||
for (let c = 0; c < columns.length; c++) {
|
||||
const lastInCol = columns[c][columns[c].length - 1];
|
||||
@@ -209,11 +203,8 @@ function layoutEvents(events) {
|
||||
return ev;
|
||||
});
|
||||
|
||||
// Calculate how many columns each event spans
|
||||
return result.map(ev => {
|
||||
const start = new Date(ev.start);
|
||||
const end = new Date(ev.end);
|
||||
// Count overlapping events
|
||||
let maxCol = ev._col;
|
||||
sorted.forEach(other => {
|
||||
if (other === ev) return;
|
||||
|
||||
Reference in New Issue
Block a user