From 5dcde0a3ef935e4a7e7038f57907e0ef4f9dacbc Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Wed, 8 Apr 2026 14:34:01 +0200 Subject: [PATCH] =?UTF-8?q?Feature:=20Enddatum=20im=20Popup=20+=20Kopieren?= =?UTF-8?q?-nach-Kalender-Button=20Enddatum=20wird=20im=20Event-Popup=20an?= =?UTF-8?q?gezeigt=20wenn=20Termin=20=C3=BCber=20Mitternacht=20geht.=20Neu?= =?UTF-8?q?er=20Kopieren-Button=20(=F0=9F=93=8B)=20im=20Popup=20=C3=B6ffne?= =?UTF-8?q?t=20Kalender-Auswahl=20und=20dupliziert=20den=20Termin=20in=20d?= =?UTF-8?q?en=20gew=C3=A4hlten=20Kalender=20(CalDAV=20/=20Lokal=20/=20Goog?= =?UTF-8?q?le).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/css/app.css | 15 ++++++++ frontend/index.html | 4 ++ frontend/js/calendar.js | 82 +++++++++++++++++++++++++++++++++++++++-- frontend/js/i18n.js | 2 + 4 files changed, 100 insertions(+), 3 deletions(-) diff --git a/frontend/css/app.css b/frontend/css/app.css index 01dfafa..939f50b 100644 --- a/frontend/css/app.css +++ b/frontend/css/app.css @@ -791,6 +791,21 @@ a { color: var(--primary); text-decoration: none; } .popup-body { padding: 12px 16px; } .popup-time, .popup-location, .popup-calendar { font-size: 13px; color: var(--text-2); margin-bottom: 6px; } .popup-description { font-size: 13px; color: var(--text-1); margin-bottom: 6px; white-space: pre-wrap; } +.popup-copy-menu { + border-top: 1px solid var(--border); + padding: 4px 0; +} +.popup-copy-label { + font-size: 11px; font-weight: 600; text-transform: uppercase; + letter-spacing: .5px; color: var(--text-3); + padding: 4px 14px 6px; +} +.popup-copy-item { + display: flex; align-items: center; gap: 9px; + padding: 7px 14px; cursor: pointer; font-size: 13px; color: var(--text-1); +} +.popup-copy-item:hover { background: var(--bg-hover); } +.popup-copy-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } /* ── Settings Page ──────────────────────────────────────── */ #modal-settings.modal-overlay { diff --git a/frontend/index.html b/frontend/index.html index 2c04d89..8b759f3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -273,6 +273,9 @@ + @@ -284,6 +287,7 @@ + diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js index 38e0488..8e2ae6a 100644 --- a/frontend/js/calendar.js +++ b/frontend/js/calendar.js @@ -731,12 +731,16 @@ function showEventPopup(ev, anchor) { // Time if (ev.allDay) { - document.getElementById('popup-time').textContent = 'Ganztägig'; + document.getElementById('popup-time').textContent = t('allday_cap'); } else { const s = new Date(ev.start); const e = new Date(ev.end); - document.getElementById('popup-time').textContent = - `${fmtDatetime(s)} – ${fmtTime(e)}`; + const sameDay = s.getFullYear() === e.getFullYear() && + s.getMonth() === e.getMonth() && + s.getDate() === e.getDate(); + document.getElementById('popup-time').textContent = sameDay + ? `${fmtDatetime(s)} – ${fmtTime(e)}` + : `${fmtDatetime(s)} – ${fmtDatetime(e)}`; } document.getElementById('popup-location').textContent = ev.location || ''; @@ -759,6 +763,33 @@ function showEventPopup(ev, anchor) { popup.classList.add('hidden'); openEditEventModal(ev); }; + + // Copy to calendar + document.getElementById('popup-copy').onclick = e => { + e.stopPropagation(); + const menu = document.getElementById('popup-copy-menu'); + if (!menu.classList.contains('hidden')) { menu.classList.add('hidden'); return; } + const targets = buildWritableCalendars(ev); + if (!targets.length) { showToast('Keine Zielkalender verfügbar', true); return; } + menu.innerHTML = `` + + targets.map(c => + `` + ).join(''); + menu.classList.remove('hidden'); + menu.querySelectorAll('.popup-copy-item').forEach(el => { + el.addEventListener('click', async ev2 => { + ev2.stopPropagation(); + menu.classList.add('hidden'); + popup.classList.add('hidden'); + const cal = targets[parseInt(el.dataset.calIdx)]; + await copyEventToCalendar(ev, cal); + }); + }); + }; + document.getElementById('popup-delete').onclick = async () => { if (!confirm(t("confirm_delete_event", {title: ev.title}))) return; popup.classList.add('hidden'); @@ -1881,3 +1912,48 @@ function fmtDatetime(d) { function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>'); } + +function buildWritableCalendars(_excludeEv) { + const list = []; + let idx = 0; + for (const acc of state.accounts) { + for (const cal of acc.calendars) { + if (cal.sidebar_hidden) continue; + list.push({ _idx: idx++, id: cal.id, name: `${acc.name} / ${cal.name}`, color: cal.color || '#4285f4', type: 'caldav' }); + } + } + for (const cal of state.localCalendars) { + list.push({ _idx: idx++, id: cal.id, name: cal.name, color: cal.color || '#34a853', type: 'local' }); + } + for (const acc of state.googleAccounts) { + for (const cal of acc.calendars) { + if (cal.sidebar_hidden) continue; + list.push({ _idx: idx++, id: cal.id, name: `${acc.email} / ${cal.name}`, color: cal.color || '#4285f4', type: 'google' }); + } + } + return list; +} + +async function copyEventToCalendar(ev, cal) { + const { title, start, end, allDay, location, description, color } = ev; + try { + if (cal.type === 'google') { + await api.post('/google/events', { + calendar_db_id: cal.id, title, start, end, allDay, + location: location || '', description: description || '', + }); + } else if (cal.type === 'local') { + await api.post('/local/events', { + calendar_id: cal.id, title, start, end, allDay, + location: location || '', description: description || '', color: color || null, + }); + } else { + await api.post('/caldav/events', { + calendar_id: cal.id, title, start, end, allDay, + location: location || '', description: description || '', color: color || null, + }); + } + showToast(t('event_copied')); + fetchAndRender(true); + } catch (e) { showToast(e.message, true); } +} diff --git a/frontend/js/i18n.js b/frontend/js/i18n.js index b57c5dc..48de8ae 100644 --- a/frontend/js/i18n.js +++ b/frontend/js/i18n.js @@ -142,6 +142,7 @@ const translations = { error_enter_title: 'Bitte Titel eingeben', error_enter_date: 'Bitte Datum eingeben', error_enter_start: 'Bitte Start-Zeit eingeben', + copy_to_calendar: 'Kopieren nach…', event_copied: 'Termin kopiert', event_updated: 'Termin aktualisiert', event_created: 'Termin erstellt', confirm_delete_event: '"{title}" wirklich löschen?', event_deleted: 'Termin gelöscht', @@ -336,6 +337,7 @@ const translations = { error_enter_title: 'Please enter a title', error_enter_date: 'Please enter a date', error_enter_start: 'Please enter a start time', + copy_to_calendar: 'Copy to…', event_copied: 'Event copied', event_updated: 'Event updated', event_created: 'Event created', confirm_delete_event: 'Really delete "{title}"?', event_deleted: 'Event deleted',