diff --git a/frontend/js/app.js b/frontend/js/app.js
index d1b7a78..718a0ca 100644
--- a/frontend/js/app.js
+++ b/frontend/js/app.js
@@ -164,12 +164,12 @@ function bindLoginForm() {
function loadAvatarImage(avatarEl, username) {
const img = new Image();
img.onload = () => {
- avatarEl.textContent = '';
- img.style.cssText = 'width:100%;height:100%;object-fit:cover;border-radius:50%';
+ avatarEl.innerHTML = '';
+ img.style.cssText = 'width:100%;height:100%;object-fit:cover;position:absolute;inset:0';
avatarEl.appendChild(img);
};
img.onerror = () => {
- // Fallback to letter
+ avatarEl.innerHTML = '';
avatarEl.textContent = (username || '?')[0].toUpperCase();
};
img.src = `/api/profile/avatar?t=${Date.now()}`;
diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js
index e70f688..159b1bb 100644
--- a/frontend/js/calendar.js
+++ b/frontend/js/calendar.js
@@ -1,9 +1,12 @@
import { api } from './api.js';
-import { applyTheme, isToday, isSameDay, toLocalDatetimeInput, toDateInput, dateKey } from './utils.js';
+import { applyTheme, isToday, isSameDay, toLocalDatetimeInput, toDateInput, dateKey, dayOfWeek, weekStart } from './utils.js';
import { renderMonth } from './views/month.js';
import { renderWeek } from './views/week.js';
import { renderAgenda } from './views/agenda.js';
+// week start day global (loaded from settings)
+let weekStartDay = 'monday';
+
const MONTHS = ['Januar','Februar','März','April','Mai','Juni',
'Juli','August','September','Oktober','November','Dezember'];
@@ -29,6 +32,7 @@ export async function initCalendar() {
state.accounts = accounts;
state.currentView = settings.default_view || 'month';
state.dimPast = settings.dim_past_events;
+ weekStartDay = settings.week_start_day || 'monday';
applyTheme(settings);
updateViewButtons();
@@ -65,13 +69,11 @@ function getViewRange() {
if (state.currentView === 'month') {
start = new Date(d.getFullYear(), d.getMonth(), 1);
- start.setDate(start.getDate() - start.getDay() - 1);
+ start.setDate(start.getDate() - dayOfWeek(start, weekStartDay) - 1);
end = new Date(d.getFullYear(), d.getMonth() + 1, 0);
- end.setDate(end.getDate() + (6 - end.getDay()) + 1);
+ end.setDate(end.getDate() + (6 - dayOfWeek(end, weekStartDay)) + 1);
} else if (state.currentView === 'week') {
- start = new Date(d);
- start.setDate(d.getDate() - d.getDay());
- start.setHours(0, 0, 0, 0);
+ start = weekStart(d, weekStartDay);
end = new Date(start);
end.setDate(start.getDate() + 7);
} else if (state.currentView === 'day') {
@@ -96,7 +98,8 @@ function renderView() {
if (state.currentView === 'month') {
renderMonth(container, state.currentDate, evs,
date => { state.currentDate = date; state.currentView = 'day'; updateViewButtons(); fetchAndRender(); },
- showEventPopup
+ showEventPopup,
+ weekStartDay
);
} else if (state.currentView === 'week') {
renderWeek(container, state.currentDate, evs,
@@ -104,13 +107,16 @@ function renderView() {
if (switchDay) { state.currentDate = date; state.currentView = 'day'; updateViewButtons(); fetchAndRender(); }
else openNewEventModal(date);
},
- showEventPopup
+ showEventPopup,
+ false,
+ weekStartDay
);
} else if (state.currentView === 'day') {
renderWeek(container, state.currentDate, evs,
(date, switchDay) => { if (!switchDay) openNewEventModal(date); },
showEventPopup,
- true
+ true,
+ weekStartDay
);
} else {
renderAgenda(container, state.currentDate, evs, showEventPopup);
@@ -133,14 +139,13 @@ function updateTitle() {
if (state.currentView === 'month') {
title = `${MONTHS[d.getMonth()]} ${d.getFullYear()}`;
} else if (state.currentView === 'week') {
- const sun = new Date(d);
- sun.setDate(d.getDate() - d.getDay());
- const sat = new Date(sun);
- sat.setDate(sun.getDate() + 6);
- const sameMonth = sun.getMonth() === sat.getMonth();
+ const mon = weekStart(d, weekStartDay);
+ const sun = new Date(mon);
+ sun.setDate(mon.getDate() + 6);
+ const sameMonth = mon.getMonth() === sun.getMonth();
title = sameMonth
- ? `${sun.getDate()}. – ${sat.getDate()}. ${MONTHS[sat.getMonth()]} ${sat.getFullYear()}`
- : `${sun.getDate()}. ${MONTHS[sun.getMonth()]} – ${sat.getDate()}. ${MONTHS[sat.getMonth()]} ${sat.getFullYear()}`;
+ ? `${mon.getDate()}. – ${sun.getDate()}. ${MONTHS[sun.getMonth()]} ${sun.getFullYear()}`
+ : `${mon.getDate()}. ${MONTHS[mon.getMonth()]} – ${sun.getDate()}. ${MONTHS[sun.getMonth()]} ${sun.getFullYear()}`;
} else if (state.currentView === 'day') {
title = `${d.getDate()}. ${MONTHS[d.getMonth()]} ${d.getFullYear()}`;
} else {
@@ -164,9 +169,15 @@ function renderMiniCal() {
`${MONTHS[miniD.getMonth()]} ${miniD.getFullYear()}`;
const firstDay = new Date(miniD.getFullYear(), miniD.getMonth(), 1);
- const lastDay = new Date(miniD.getFullYear(), miniD.getMonth() + 1, 0);
const gridStart = new Date(firstDay);
- gridStart.setDate(gridStart.getDate() - firstDay.getDay());
+ gridStart.setDate(gridStart.getDate() - dayOfWeek(firstDay, weekStartDay));
+
+ // Update mini-cal DOW headers based on weekStartDay
+ const miniDowEls = document.querySelectorAll('.mini-cal-grid .mini-dow');
+ const DOW_MONDAY = ['Mo','Di','Mi','Do','Fr','Sa','So'];
+ const DOW_SUNDAY = ['So','Mo','Di','Mi','Do','Fr','Sa'];
+ const DOW_LABELS = weekStartDay === 'sunday' ? DOW_SUNDAY : DOW_MONDAY;
+ miniDowEls.forEach((el, i) => { el.textContent = DOW_LABELS[i]; });
// Build event date set
const eventDates = new Set(state.events.map(ev => {
@@ -618,10 +629,11 @@ function bindAccountModal() {
// ── Settings Modal ────────────────────────────────────────
function openSettingsModal() {
const s = state.settings;
- document.getElementById('cfg-default-view').value = s.default_view || 'month';
- document.getElementById('cfg-primary-color').value = s.primary_color || '#4285f4';
- document.getElementById('cfg-accent-color').value = s.accent_color || '#ea4335';
- document.getElementById('cfg-today-color').value = s.today_color || '#4285f4';
+ document.getElementById('cfg-default-view').value = s.default_view || 'month';
+ document.getElementById('cfg-week-start').value = s.week_start_day || 'monday';
+ document.getElementById('cfg-primary-color').value = s.primary_color || '#4285f4';
+ document.getElementById('cfg-accent-color').value = s.accent_color || '#ea4335';
+ document.getElementById('cfg-today-color').value = s.today_color || '#4285f4';
document.getElementById('cfg-dim-past').checked = !!s.dim_past_events;
document.getElementById('cfg-primary-label').textContent = s.primary_color || '#4285f4';
@@ -701,20 +713,23 @@ function bindSettingsModal() {
document.getElementById('settings-save').onclick = async () => {
const settings = {
- default_view: document.getElementById('cfg-default-view').value,
- primary_color: document.getElementById('cfg-primary-color').value,
- accent_color: document.getElementById('cfg-accent-color').value,
- today_color: document.getElementById('cfg-today-color').value,
+ default_view: document.getElementById('cfg-default-view').value,
+ week_start_day: document.getElementById('cfg-week-start').value,
+ primary_color: document.getElementById('cfg-primary-color').value,
+ accent_color: document.getElementById('cfg-accent-color').value,
+ today_color: document.getElementById('cfg-today-color').value,
dim_past_events: document.getElementById('cfg-dim-past').checked,
};
try {
await api.put('/settings/', settings);
state.settings = { ...state.settings, ...settings };
state.dimPast = settings.dim_past_events;
+ weekStartDay = settings.week_start_day;
applyTheme(settings);
showToast('Einstellungen gespeichert');
closeModal('modal-settings');
- renderView();
+ renderMiniCal();
+ fetchAndRender();
} catch (e) { showToast(e.message, true); }
};
}
@@ -885,12 +900,27 @@ function updateTopbarAvatar(hasAvatar) {
const avatar = document.getElementById('user-avatar');
if (hasAvatar) {
const img = new Image();
- img.onload = () => { avatar.textContent = ''; img.style.cssText = 'width:100%;height:100%;object-fit:cover;border-radius:50%'; avatar.appendChild(img); };
- img.onerror = () => { const u = JSON.parse(localStorage.getItem('user')||'{}'); avatar.textContent = (u.username||'?')[0].toUpperCase(); };
+ img.onload = () => {
+ avatar.innerHTML = '';
+ img.style.cssText = 'width:100%;height:100%;object-fit:cover;position:absolute;inset:0';
+ avatar.appendChild(img);
+ };
+ img.onerror = () => {
+ avatar.innerHTML = '';
+ const u = JSON.parse(localStorage.getItem('user')||'{}');
+ avatar.textContent = (u.username||'?')[0].toUpperCase();
+ };
img.src = `/api/profile/avatar?t=${Date.now()}`;
+ // Update localStorage so avatar persists across reloads
+ const u = JSON.parse(localStorage.getItem('user')||'{}');
+ u.has_avatar = true;
+ localStorage.setItem('user', JSON.stringify(u));
} else {
+ avatar.innerHTML = '';
const user = JSON.parse(localStorage.getItem('user') || '{}');
avatar.textContent = (user.username || '?')[0].toUpperCase();
+ user.has_avatar = false;
+ localStorage.setItem('user', JSON.stringify(user));
}
}
@@ -969,25 +999,34 @@ function openCropModal(file) {
const reader = new FileReader();
reader.onload = (e) => {
const cropImg = document.getElementById('crop-image');
- cropImg.src = e.target.result;
-
- openModal('modal-crop');
// Destroy previous cropper if any
if (activeCropper) { activeCropper.destroy(); activeCropper = null; }
- // Wait for image to load then init cropper
- cropImg.onload = () => {
- activeCropper = new Cropper(cropImg, {
- aspectRatio: 1,
- viewMode: 1,
- dragMode: 'move',
- autoCropArea: 1,
- cropBoxResizable: true,
- cropBoxMovable: true,
- background: false,
- });
- };
+ // Reset image src first to force reload
+ cropImg.removeAttribute('src');
+
+ openModal('modal-crop');
+
+ // Use requestAnimationFrame to ensure modal is visible before initializing cropper
+ requestAnimationFrame(() => {
+ cropImg.onload = () => {
+ // Small delay to ensure the image is fully rendered in the DOM
+ setTimeout(() => {
+ if (activeCropper) { activeCropper.destroy(); activeCropper = null; }
+ activeCropper = new Cropper(cropImg, {
+ aspectRatio: 1,
+ viewMode: 1,
+ dragMode: 'move',
+ autoCropArea: 1,
+ cropBoxResizable: true,
+ cropBoxMovable: true,
+ background: false,
+ });
+ }, 100);
+ };
+ cropImg.src = e.target.result;
+ });
};
reader.readAsDataURL(file);
}
diff --git a/frontend/js/color-picker.js b/frontend/js/color-picker.js
new file mode 100644
index 0000000..eb07c03
--- /dev/null
+++ b/frontend/js/color-picker.js
@@ -0,0 +1,248 @@
+/* ── Gradient Color Picker (Dark Mode) ─────────────────────
+ Usage: const hex = await openColorPicker(anchorEl, '#4285f4');
+ Returns hex string or null if cancelled.
+────────────────────────────────────────────────────────── */
+
+// ── HSV ↔ RGB helpers ─────────────────────────────────────
+function hsvToRgb(h, s, v) {
+ h = h / 360 * 6;
+ const i = Math.floor(h), f = h - i, p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s);
+ let r, g, b;
+ switch (i % 6) {
+ case 0: r = v; g = t; b = p; break;
+ case 1: r = q; g = v; b = p; break;
+ case 2: r = p; g = v; b = t; break;
+ case 3: r = p; g = q; b = v; break;
+ case 4: r = t; g = p; b = v; break;
+ case 5: r = v; g = p; b = q; break;
+ }
+ return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
+}
+
+function rgbToHsv(r, g, b) {
+ r /= 255; g /= 255; b /= 255;
+ const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min;
+ let h = 0, s = max === 0 ? 0 : d / max, v = max;
+ if (d !== 0) {
+ switch (max) {
+ case r: h = ((g - b) / d + (g < b ? 6 : 0)); break;
+ case g: h = ((b - r) / d + 2); break;
+ case b: h = ((r - g) / d + 4); break;
+ }
+ h *= 60;
+ }
+ return [h, s, v];
+}
+
+function hexToRgb(hex) {
+ hex = hex.replace('#', '');
+ if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
+ return [parseInt(hex.slice(0,2),16), parseInt(hex.slice(2,4),16), parseInt(hex.slice(4,6),16)];
+}
+
+function rgbToHex(r, g, b) {
+ return '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
+}
+
+// ── Active picker tracking ────────────────────────────────
+let activePicker = null;
+let activeOutsideHandler = null;
+
+function closeActivePicker() {
+ if (activePicker) {
+ activePicker.remove();
+ activePicker = null;
+ }
+ if (activeOutsideHandler) {
+ document.removeEventListener('mousedown', activeOutsideHandler);
+ activeOutsideHandler = null;
+ }
+}
+
+// ── Main export ───────────────────────────────────────────
+export function openColorPicker(anchorEl, currentColor = '#4285f4') {
+ closeActivePicker();
+
+ return new Promise((resolve) => {
+ let [h, s, v] = rgbToHsv(...hexToRgb(currentColor));
+
+ // ── Build DOM ─────────────────────────────────────────
+ const picker = document.createElement('div');
+ picker.className = 'gcp';
+ picker.innerHTML = `
+
+
+
+
+
+ `;
+
+ const svCanvas = picker.querySelector('.gcp-sv');
+ const svCtx = svCanvas.getContext('2d', { willReadFrequently: true });
+ const svCursor = picker.querySelector('.gcp-sv-cursor');
+ const hueCanvas = picker.querySelector('.gcp-hue');
+ const hueCtx = hueCanvas.getContext('2d');
+ const hueThumb = picker.querySelector('.gcp-hue-thumb');
+ const preview = picker.querySelector('.gcp-preview');
+ const hexInput = picker.querySelector('.gcp-hex');
+ const selectBtn = picker.querySelector('.gcp-select');
+
+ // ── Draw functions ────────────────────────────────────
+ function drawSV() {
+ const w = svCanvas.width, hh = svCanvas.height;
+ // Base hue color
+ const [r, g, b] = hsvToRgb(h, 1, 1);
+ // Horizontal: white → hue color (saturation)
+ const gradH = svCtx.createLinearGradient(0, 0, w, 0);
+ gradH.addColorStop(0, '#fff');
+ gradH.addColorStop(1, `rgb(${r},${g},${b})`);
+ svCtx.fillStyle = gradH;
+ svCtx.fillRect(0, 0, w, hh);
+ // Vertical: transparent → black (value)
+ const gradV = svCtx.createLinearGradient(0, 0, 0, hh);
+ gradV.addColorStop(0, 'rgba(0,0,0,0)');
+ gradV.addColorStop(1, '#000');
+ svCtx.fillStyle = gradV;
+ svCtx.fillRect(0, 0, w, hh);
+ }
+
+ function drawHue() {
+ const w = hueCanvas.width, hh = hueCanvas.height;
+ const grad = hueCtx.createLinearGradient(0, 0, w, 0);
+ for (let i = 0; i <= 6; i++) {
+ const [r, g, b] = hsvToRgb(i * 60, 1, 1);
+ grad.addColorStop(i / 6, `rgb(${r},${g},${b})`);
+ }
+ hueCtx.fillStyle = grad;
+ hueCtx.fillRect(0, 0, w, hh);
+ }
+
+ function updateUI() {
+ const [r, g, b] = hsvToRgb(h, s, v);
+ const hex = rgbToHex(r, g, b);
+ // SV cursor position
+ svCursor.style.left = (s * svCanvas.width) + 'px';
+ svCursor.style.top = ((1 - v) * svCanvas.height) + 'px';
+ // Hue thumb position
+ hueThumb.style.left = (h / 360 * hueCanvas.width) + 'px';
+ // Preview + hex
+ preview.style.background = hex;
+ hexInput.value = hex.toUpperCase();
+ }
+
+ // ── SV interaction ────────────────────────────────────
+ function handleSV(e) {
+ const rect = svCanvas.getBoundingClientRect();
+ const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
+ const y = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
+ s = x / rect.width;
+ v = 1 - y / rect.height;
+ updateUI();
+ }
+
+ svCanvas.addEventListener('mousedown', (e) => {
+ e.preventDefault();
+ handleSV(e);
+ const move = (ev) => handleSV(ev);
+ const up = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); };
+ document.addEventListener('mousemove', move);
+ document.addEventListener('mouseup', up);
+ });
+
+ // Touch support for SV
+ svCanvas.addEventListener('touchstart', (e) => {
+ e.preventDefault();
+ const touch = (ev) => { const t = ev.touches[0]; handleSV(t); };
+ touch(e);
+ const end = () => { document.removeEventListener('touchmove', touch); document.removeEventListener('touchend', end); };
+ document.addEventListener('touchmove', touch);
+ document.addEventListener('touchend', end);
+ });
+
+ // ── Hue interaction ───────────────────────────────────
+ function handleHue(e) {
+ const rect = hueCanvas.getBoundingClientRect();
+ const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
+ h = (x / rect.width) * 360;
+ drawSV();
+ updateUI();
+ }
+
+ const hueTrack = picker.querySelector('.gcp-hue-track');
+ hueTrack.addEventListener('mousedown', (e) => {
+ e.preventDefault();
+ handleHue(e);
+ const move = (ev) => handleHue(ev);
+ const up = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); };
+ document.addEventListener('mousemove', move);
+ document.addEventListener('mouseup', up);
+ });
+
+ hueTrack.addEventListener('touchstart', (e) => {
+ e.preventDefault();
+ const touch = (ev) => { const t = ev.touches[0]; handleHue(t); };
+ touch(e);
+ const end = () => { document.removeEventListener('touchmove', touch); document.removeEventListener('touchend', end); };
+ document.addEventListener('touchmove', touch);
+ document.addEventListener('touchend', end);
+ });
+
+ // ── Hex input ─────────────────────────────────────────
+ hexInput.addEventListener('change', () => {
+ let val = hexInput.value.trim();
+ if (!val.startsWith('#')) val = '#' + val;
+ if (/^#[0-9a-fA-F]{6}$/.test(val)) {
+ [h, s, v] = rgbToHsv(...hexToRgb(val));
+ drawSV();
+ updateUI();
+ }
+ });
+
+ // ── Select button ─────────────────────────────────────
+ selectBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const [r, g, b] = hsvToRgb(h, s, v);
+ closeActivePicker();
+ resolve(rgbToHex(r, g, b));
+ });
+
+ // ── Position picker ───────────────────────────────────
+ document.body.appendChild(picker);
+ activePicker = picker;
+
+ const anchorRect = anchorEl.getBoundingClientRect();
+ let top = anchorRect.bottom + 6;
+ let left = anchorRect.left;
+
+ // Keep in viewport
+ const pRect = picker.getBoundingClientRect();
+ if (left + pRect.width > window.innerWidth - 8) left = window.innerWidth - pRect.width - 8;
+ if (left < 8) left = 8;
+ if (top + pRect.height > window.innerHeight - 8) top = anchorRect.top - pRect.height - 6;
+
+ picker.style.top = top + 'px';
+ picker.style.left = left + 'px';
+
+ // ── Initial draw ──────────────────────────────────────
+ drawSV();
+ drawHue();
+ updateUI();
+
+ // ── Close on outside click ────────────────────────────
+ setTimeout(() => {
+ activeOutsideHandler = (e) => {
+ if (!picker.contains(e.target) && e.target !== anchorEl && !anchorEl.contains(e.target)) {
+ closeActivePicker();
+ resolve(null);
+ }
+ };
+ document.addEventListener('mousedown', activeOutsideHandler);
+ }, 0);
+ });
+}
diff --git a/frontend/js/utils.js b/frontend/js/utils.js
index aafc823..5306d03 100644
--- a/frontend/js/utils.js
+++ b/frontend/js/utils.js
@@ -37,6 +37,32 @@ export function toDateInput(d) {
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
}
+// Monday-first: returns 0=Mo, 1=Di, ..., 6=So
+export function dayOfWeek(d, weekStartDay = 'monday') {
+ if (weekStartDay === 'sunday') {
+ return d.getDay(); // 0=So, 1=Mo, ..., 6=Sa
+ }
+ return (d.getDay() + 6) % 7; // 0=Mo, 1=Di, ..., 6=So
+}
+
+// Returns the start-of-week date for d
+export function weekStart(d, weekStartDay = 'monday') {
+ const m = new Date(d);
+ m.setDate(m.getDate() - dayOfWeek(m, weekStartDay));
+ m.setHours(0, 0, 0, 0);
+ return m;
+}
+
+// Returns the ISO week number (Monday-based, ISO 8601)
+export function getISOWeekNumber(d) {
+ const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
+ // ISO week: weeks start on Monday, week 1 contains the first Thursday
+ const day = date.getUTCDay() || 7; // make Sunday = 7
+ date.setUTCDate(date.getUTCDate() + 4 - day);
+ const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
+ return Math.ceil((((date - yearStart) / 86400000) + 1) / 7);
+}
+
export function applyTheme(settings) {
const root = document.documentElement;
root.style.setProperty('--primary', settings.primary_color || '#4285f4');
diff --git a/frontend/js/views/month.js b/frontend/js/views/month.js
index 6ff1450..13de6f3 100644
--- a/frontend/js/views/month.js
+++ b/frontend/js/views/month.js
@@ -1,17 +1,20 @@
-import { formatDate, isSameDay, isToday, isPast } from '../utils.js';
+import { formatDate, isSameDay, isToday, isPast, dayOfWeek, getISOWeekNumber } from '../utils.js';
-const DOW = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
+const DOW_MONDAY = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
+const DOW_SUNDAY = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
-export function renderMonth(container, currentDate, events, onDayClick, onEventClick) {
+export function renderMonth(container, currentDate, events, onDayClick, onEventClick, weekStartDay = 'monday') {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
+ const DOW = weekStartDay === 'sunday' ? DOW_SUNDAY : DOW_MONDAY;
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
- // Start grid on Sunday of the week containing the 1st
+ // Start grid on the correct weekday
const gridStart = new Date(firstDay);
- gridStart.setDate(gridStart.getDate() - firstDay.getDay());
+ const offset = dayOfWeek(firstDay, weekStartDay);
+ gridStart.setDate(gridStart.getDate() - offset);
const cells = [];
const d = new Date(gridStart);
@@ -39,45 +42,55 @@ export function renderMonth(container, currentDate, events, onDayClick, onEventC
}
});
- // Header
- const headerHtml = DOW.map(d => `
${d}
`).join('');
+ // Header: KW-Spalte + Wochentage
+ const headerHtml = `` +
+ DOW.map(d => `
${d}
`).join('');
- // Cells
- const cellsHtml = cells.map(cell => {
- const key = dateKey(cell);
- const cellEvs = (evMap[key] || []).slice().sort((a, b) => {
- if (a.allDay && !b.allDay) return -1;
- if (!a.allDay && b.allDay) return 1;
- return new Date(a.start) - new Date(b.start);
- });
+ // Build rows (6 weeks × 7 days)
+ let cellsHtml = '';
+ for (let row = 0; row < 6; row++) {
+ // KW cell for the first day of this row
+ const rowFirstDay = cells[row * 7];
+ const kw = getISOWeekNumber(rowFirstDay);
+ cellsHtml += `
${kw}
`;
- const isOther = cell.getMonth() !== month;
- const todayClass = isToday(cell) ? 'today' : '';
- const otherClass = isOther ? 'other-month' : '';
- const numClass = isToday(cell) ? 'today' : '';
+ for (let col = 0; col < 7; col++) {
+ const cell = cells[row * 7 + col];
+ const key = dateKey(cell);
+ const cellEvs = (evMap[key] || []).slice().sort((a, b) => {
+ if (a.allDay && !b.allDay) return -1;
+ if (!a.allDay && b.allDay) return 1;
+ return new Date(a.start) - new Date(b.start);
+ });
- const MAX_VISIBLE = 3;
- const visible = cellEvs.slice(0, MAX_VISIBLE);
- const hiddenCount = cellEvs.length - MAX_VISIBLE;
+ const isOther = cell.getMonth() !== month;
+ const todayClass = isToday(cell) ? 'today' : '';
+ const otherClass = isOther ? 'other-month' : '';
+ const numClass = isToday(cell) ? 'today' : '';
- const evHtml = visible.map(ev => {
- const color = ev.color || ev.calendarColor || '#4285f4';
- const pastClass = isPast(ev) ? 'past' : '';
- const title = ev.allDay ? ev.title : `${fmtTime(new Date(ev.start))} ${ev.title}`;
- return `
${escHtml(title)}
`;
- }).join('');
+ const MAX_VISIBLE = 3;
+ const visible = cellEvs.slice(0, MAX_VISIBLE);
+ const hiddenCount = cellEvs.length - MAX_VISIBLE;
- const moreHtml = hiddenCount > 0
- ? `
+${hiddenCount} weitere
`
- : '';
+ const evHtml = visible.map(ev => {
+ const color = ev.color || ev.calendarColor || '#4285f4';
+ const pastClass = isPast(ev) ? 'past' : '';
+ const title = ev.allDay ? ev.title : `${fmtTime(new Date(ev.start))} ${ev.title}`;
+ return `
${escHtml(title)}
`;
+ }).join('');
- return `
-
${cell.getDate()}
- ${evHtml}${moreHtml}
-
`;
- }).join('');
+ const moreHtml = hiddenCount > 0
+ ? `
+${hiddenCount} weitere
`
+ : '';
+
+ cellsHtml += `
+
${cell.getDate()}
+ ${evHtml}${moreHtml}
+
`;
+ }
+ }
container.innerHTML = `
diff --git a/frontend/js/views/week.js b/frontend/js/views/week.js
index 27018d8..301603c 100644
--- a/frontend/js/views/week.js
+++ b/frontend/js/views/week.js
@@ -1,17 +1,16 @@
-import { isToday, isPast } from '../utils.js';
+import { isToday, isPast, dayOfWeek, weekStart, getISOWeekNumber } from '../utils.js';
const DOW_SHORT = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
-export function renderWeek(container, currentDate, events, onSlotClick, onEventClick, isSingleDay = false) {
+export function renderWeek(container, currentDate, events, onSlotClick, onEventClick, isSingleDay = false, weekStartDay = 'monday') {
// Build the days array (7 days for week, 1 for day)
const days = [];
if (isSingleDay) {
days.push(new Date(currentDate));
} else {
- const sunday = new Date(currentDate);
- sunday.setDate(sunday.getDate() - sunday.getDay());
+ const monday = weekStart(currentDate, weekStartDay);
for (let i = 0; i < 7; i++) {
- const d = new Date(sunday);
+ const d = new Date(monday);
d.setDate(d.getDate() + i);
days.push(d);
}
@@ -21,6 +20,12 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
const allDayEvs = events.filter(ev => ev.allDay);
const timedEvs = events.filter(ev => !ev.allDay);
+ // ── KW Badge ──────────────────────────────────────────
+ const kwNum = getISOWeekNumber(days[0]);
+ const kwBadge = !isSingleDay
+ ? `
KW ${kwNum}
`
+ : '';
+
// ── Header ────────────────────────────────────────────
const headerCols = days.map(day => {
const todayCls = isToday(day) ? 'today' : '';
@@ -98,7 +103,7 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC
container.innerHTML = `