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',