757 lines
27 KiB
HTML
757 lines
27 KiB
HTML
<!-- Calendar Section -->
|
|
<section id="calendar-section" class="tab-content">
|
|
<div class="container">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h3 class="mb-0">Kalender</h3>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<select id="calendar-location-filter" class="form-select form-select-sm">
|
|
<option value="">Ort (alle)</option>
|
|
</select>
|
|
{% if admin_enabled %}
|
|
<div class="btn-group" role="group">
|
|
<button id="export-calendar-btn" class="btn btn-outline-secondary btn-sm" title="Kalender als Excel herunterladen">
|
|
Export
|
|
</button>
|
|
<button id="import-calendar-btn" class="btn btn-outline-secondary btn-sm" title="Excel hochladen und Kalender überschreiben">
|
|
Import
|
|
</button>
|
|
</div>
|
|
<button id="add-calendar-entry-btn" class="btn btn-primary" title="Eintrag hinzufügen">
|
|
<i class="bi bi-plus-circle"></i>
|
|
</button>
|
|
<input type="file" id="import-calendar-input" accept=".xlsx" style="display:none">
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="calendar-weekday-header"></div>
|
|
|
|
<div id="calendar-days" class="calendar-grid">
|
|
<!-- Populated via inline script below -->
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Calendar Modal -->
|
|
<div class="modal fade" id="calendarModal" tabindex="-1" aria-labelledby="calendarModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="calendarModalLabel">Kalendereintrag</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="calendarForm">
|
|
<input type="hidden" id="calendarEntryId">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label for="calendarDate" class="form-label">Datum</label>
|
|
<input type="date" class="form-control" id="calendarDate" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label for="calendarTime" class="form-label">Uhrzeit</label>
|
|
<input type="time" class="form-control" id="calendarTime">
|
|
</div>
|
|
</div>
|
|
{% if admin_enabled %}
|
|
<div class="row g-3 mt-1" id="calendarRecurrenceRow">
|
|
<div class="col-md-6">
|
|
<label for="calendarRepeatCount" class="form-label">Wöchentliche Wiederholung</label>
|
|
<input type="number" class="form-control" id="calendarRepeatCount" min="1" max="52" step="1" value="1">
|
|
<div class="form-text">Legt einzelne wöchentliche Termine an (für Nachbearbeitung).</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
<div class="mt-3">
|
|
<label for="calendarTitle" class="form-label">Titel</label>
|
|
<input type="text" class="form-control" id="calendarTitle" required>
|
|
</div>
|
|
<div class="mt-3">
|
|
<label for="calendarLocation" class="form-label">Ort</label>
|
|
<input type="text" class="form-control" id="calendarLocation">
|
|
</div>
|
|
<div class="mt-3">
|
|
<label for="calendarDetails" class="form-label">Details</label>
|
|
<textarea class="form-control" id="calendarDetails" rows="3"></textarea>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
<button type="button" class="btn btn-primary" id="saveCalendarEntryBtn">Speichern</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const toLocalISO = (d) => {
|
|
const year = d.getFullYear();
|
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
const day = String(d.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
};
|
|
|
|
const holidayCache = new Map();
|
|
|
|
function calculateEasterSunday(year) {
|
|
const a = year % 19;
|
|
const b = Math.floor(year / 100);
|
|
const c = year % 100;
|
|
const d = Math.floor(b / 4);
|
|
const e = b % 4;
|
|
const f = Math.floor((b + 8) / 25);
|
|
const g = Math.floor((b - f + 1) / 3);
|
|
const h = (19 * a + b - d - g + 15) % 30;
|
|
const i = Math.floor(c / 4);
|
|
const k = c % 4;
|
|
const l = (32 + 2 * e + 2 * i - h - k) % 7;
|
|
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
|
const month = Math.floor((h + l - 7 * m + 114) / 31);
|
|
const day = ((h + l - 7 * m + 114) % 31) + 1;
|
|
return new Date(year, month - 1, day);
|
|
}
|
|
|
|
function getGermanHolidays(year) {
|
|
if (holidayCache.has(year)) return holidayCache.get(year);
|
|
|
|
const holidays = new Map();
|
|
const add = (month, day, name) => {
|
|
holidays.set(toLocalISO(new Date(year, month - 1, day)), name);
|
|
};
|
|
|
|
add(1, 1, 'Neujahr');
|
|
add(5, 1, 'Tag der Arbeit');
|
|
add(10, 3, 'Tag der Deutschen Einheit');
|
|
add(12, 25, '1. Weihnachtstag');
|
|
add(12, 26, '2. Weihnachtstag');
|
|
|
|
const easterSunday = calculateEasterSunday(year);
|
|
const addOffset = (offset, name) => {
|
|
const d = new Date(easterSunday);
|
|
d.setDate(easterSunday.getDate() + offset);
|
|
holidays.set(toLocalISO(d), name);
|
|
};
|
|
|
|
addOffset(-2, 'Karfreitag');
|
|
addOffset(0, 'Ostersonntag');
|
|
addOffset(1, 'Ostermontag');
|
|
addOffset(39, 'Christi Himmelfahrt');
|
|
addOffset(49, 'Pfingstsonntag');
|
|
addOffset(50, 'Pfingstmontag');
|
|
|
|
holidayCache.set(year, holidays);
|
|
return holidays;
|
|
}
|
|
|
|
function getHolidayName(date) {
|
|
const holidays = getGermanHolidays(date.getFullYear());
|
|
return holidays.get(toLocalISO(date)) || '';
|
|
}
|
|
|
|
function isDesktopView() {
|
|
return window.matchMedia && window.matchMedia('(min-width: 992px)').matches;
|
|
}
|
|
|
|
function getCalendarRange() {
|
|
const weeks = window._calendarIsAdmin ? 12 : 4;
|
|
const start = new Date();
|
|
start.setHours(0, 0, 0, 0);
|
|
|
|
if (isDesktopView()) {
|
|
// shift back to Monday (0 = Sunday)
|
|
const diffToMonday = (start.getDay() + 6) % 7;
|
|
start.setDate(start.getDate() - diffToMonday);
|
|
}
|
|
|
|
const end = new Date(start);
|
|
end.setDate(end.getDate() + (weeks * 7) - 1);
|
|
return { start, end };
|
|
}
|
|
|
|
function renderWeekdayHeader(startDate) {
|
|
const header = document.querySelector('.calendar-weekday-header');
|
|
if (!header) return;
|
|
header.innerHTML = '';
|
|
const formatDayShort = new Intl.DateTimeFormat('de-DE', { weekday: 'short' });
|
|
for (let i = 0; i < 7; i++) {
|
|
const d = new Date(startDate);
|
|
d.setDate(startDate.getDate() + i);
|
|
const div = document.createElement('div');
|
|
div.textContent = formatDayShort.format(d);
|
|
header.appendChild(div);
|
|
}
|
|
}
|
|
|
|
function buildCalendarStructure(force = false) {
|
|
const daysContainer = document.getElementById('calendar-days');
|
|
if (!daysContainer || (daysContainer.childElementCount > 0 && !force)) return;
|
|
|
|
const isAdmin = typeof admin_enabled !== 'undefined' && admin_enabled;
|
|
const calendarColSpan = 3;
|
|
window._calendarColSpan = calendarColSpan;
|
|
window._calendarIsAdmin = isAdmin;
|
|
daysContainer.innerHTML = '';
|
|
|
|
const formatDayName = new Intl.DateTimeFormat('de-DE', { weekday: 'long' });
|
|
const formatDate = new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
const { start, end } = getCalendarRange();
|
|
const todayIso = toLocalISO(new Date());
|
|
renderWeekdayHeader(start);
|
|
const dayCount = Math.round((end - start) / (1000 * 60 * 60 * 24)) + 1;
|
|
|
|
for (let i = 0; i < dayCount; i++) {
|
|
const date = new Date(start);
|
|
date.setDate(start.getDate() + i);
|
|
const isoDate = toLocalISO(date);
|
|
const isSunday = date.getDay() === 0;
|
|
const holidayName = getHolidayName(date);
|
|
const holidayBadge = holidayName ? `<span class="calendar-holiday-badge">${holidayName}</span>` : '';
|
|
|
|
const dayBlock = document.createElement('div');
|
|
dayBlock.className = 'calendar-day empty';
|
|
dayBlock.dataset.date = isoDate;
|
|
if (isoDate === toLocalISO(new Date())) {
|
|
dayBlock.classList.add('calendar-day-today');
|
|
}
|
|
if (isSunday) {
|
|
dayBlock.classList.add('calendar-day-sunday');
|
|
}
|
|
if (holidayName) {
|
|
dayBlock.classList.add('calendar-day-holiday');
|
|
}
|
|
if (isDesktopView() && isoDate < todayIso) {
|
|
dayBlock.classList.add('calendar-day-hidden');
|
|
}
|
|
|
|
dayBlock.innerHTML = `
|
|
<div class="calendar-day-header">
|
|
<div class="calendar-day-name">${formatDayName.format(date)}</div>
|
|
<div class="calendar-day-date">${formatDate.format(date)}${holidayBadge}</div>
|
|
</div>
|
|
<div class="table-responsive calendar-table-wrapper">
|
|
<table class="table table-sm mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Zeit</th>
|
|
<th scope="col">Titel</th>
|
|
<th scope="col">Ort</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="calendar-entries-list">
|
|
<tr class="calendar-empty-row">
|
|
<td colspan="${calendarColSpan}" class="text-center text-muted">Keine Einträge</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
|
|
daysContainer.appendChild(dayBlock);
|
|
}
|
|
}
|
|
|
|
function resetDayBlock(block) {
|
|
const tbody = block.querySelector('.calendar-entries-list');
|
|
if (!tbody) return;
|
|
block.classList.add('empty');
|
|
tbody.innerHTML = `
|
|
<tr class="calendar-empty-row">
|
|
<td colspan="${window._calendarColSpan || 3}" class="text-center text-muted">Keine Einträge</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
function clearCalendarEntries() {
|
|
document.querySelectorAll('.calendar-day').forEach(resetDayBlock);
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
return (str || '').replace(/[&<>"']/g, (c) => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
}[c]));
|
|
}
|
|
|
|
function createEmptyRow() {
|
|
const row = document.createElement('tr');
|
|
row.className = 'calendar-empty-row';
|
|
row.innerHTML = `<td colspan="${window._calendarColSpan || 3}" class="text-center text-muted">Keine Einträge</td>`;
|
|
return row;
|
|
}
|
|
|
|
function formatDisplayTime(timeStr) {
|
|
if (!timeStr) return '';
|
|
const match = String(timeStr).match(/^(\d{1,2}):(\d{2})(?::\d{2})?$/);
|
|
if (match) {
|
|
const h = match[1].padStart(2, '0');
|
|
const m = match[2];
|
|
return `${h}:${m}`;
|
|
}
|
|
return timeStr;
|
|
}
|
|
|
|
// Location filter helpers
|
|
let locationOptions = new Set();
|
|
|
|
function resetLocationFilterOptions() {
|
|
locationOptions = new Set();
|
|
const select = document.getElementById('calendar-location-filter');
|
|
if (!select) return;
|
|
select.innerHTML = '<option value="">Ort (alle)</option>';
|
|
}
|
|
|
|
function addLocationOption(location) {
|
|
if (!location) return;
|
|
const normalized = location.trim();
|
|
if (!normalized || locationOptions.has(normalized)) return;
|
|
locationOptions.add(normalized);
|
|
const select = document.getElementById('calendar-location-filter');
|
|
if (!select) return;
|
|
const opt = document.createElement('option');
|
|
opt.value = normalized;
|
|
opt.textContent = normalized;
|
|
select.appendChild(opt);
|
|
}
|
|
|
|
function applyLocationFilter() {
|
|
const select = document.getElementById('calendar-location-filter');
|
|
const filterValue = select ? select.value : '';
|
|
|
|
document.querySelectorAll('.calendar-day').forEach(day => {
|
|
const tbody = day.querySelector('.calendar-entries-list');
|
|
if (!tbody) return;
|
|
|
|
// Remove any stale empty rows before recalculating
|
|
tbody.querySelectorAll('.calendar-empty-row').forEach(r => r.remove());
|
|
|
|
let visibleCount = 0;
|
|
const entryRows = tbody.querySelectorAll('tr.calendar-entry-row');
|
|
entryRows.forEach(row => {
|
|
const rowLoc = (row.dataset.location || '').trim();
|
|
const match = !filterValue || rowLoc === filterValue;
|
|
row.style.display = match ? '' : 'none';
|
|
|
|
const detailsRow = row.nextElementSibling && row.nextElementSibling.classList.contains('calendar-details-row')
|
|
? row.nextElementSibling
|
|
: null;
|
|
if (detailsRow) {
|
|
const showDetails = match && detailsRow.dataset.expanded === 'true';
|
|
detailsRow.style.display = showDetails ? '' : 'none';
|
|
}
|
|
|
|
if (match) visibleCount += 1;
|
|
});
|
|
|
|
if (visibleCount === 0) {
|
|
day.classList.add('empty');
|
|
tbody.appendChild(createEmptyRow());
|
|
} else {
|
|
day.classList.remove('empty');
|
|
}
|
|
});
|
|
}
|
|
|
|
function removeCalendarEntryFromUI(id) {
|
|
if (!id) return;
|
|
const selector = `.calendar-entry-row[data-id="${id}"], .calendar-details-row[data-parent-id="${id}"]`;
|
|
const rows = document.querySelectorAll(selector);
|
|
rows.forEach(row => {
|
|
const tbody = row.parentElement;
|
|
const dayBlock = row.closest('.calendar-day');
|
|
row.remove();
|
|
const remainingEntries = tbody ? tbody.querySelectorAll('tr.calendar-entry-row').length : 0;
|
|
if (remainingEntries === 0 && dayBlock) {
|
|
resetDayBlock(dayBlock);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Helper to insert entries sorted by time inside an existing day block
|
|
function addCalendarEntry(entry) {
|
|
if (!entry || !entry.date) return;
|
|
const block = document.querySelector(`.calendar-day[data-date="${entry.date}"]`);
|
|
if (!block) return;
|
|
|
|
const tbody = block.querySelector('.calendar-entries-list');
|
|
if (!tbody) return;
|
|
|
|
const emptyRow = tbody.querySelector('.calendar-empty-row');
|
|
if (emptyRow) emptyRow.remove();
|
|
block.classList.remove('empty');
|
|
|
|
const row = document.createElement('tr');
|
|
const timeValue = formatDisplayTime(entry.time || '');
|
|
row.dataset.time = timeValue;
|
|
row.dataset.id = entry.id || '';
|
|
row.dataset.date = entry.date;
|
|
row.dataset.title = entry.title || '';
|
|
row.dataset.location = entry.location || '';
|
|
row.dataset.details = entry.details || '';
|
|
row.className = 'calendar-entry-row';
|
|
row.innerHTML = `
|
|
<td>${timeValue || '-'}</td>
|
|
<td>${entry.title || ''} ${entry.details ? '<span class="details-indicator" title="Details vorhanden">ⓘ</span>' : ''}</td>
|
|
<td>${entry.location || ''}</td>
|
|
`;
|
|
|
|
const rows = Array.from(tbody.querySelectorAll('tr.calendar-entry-row'));
|
|
const insertIndex = rows.findIndex(r => {
|
|
const existingTime = r.dataset.time || '';
|
|
return timeValue && (!existingTime || timeValue < existingTime);
|
|
});
|
|
|
|
if (insertIndex === -1) {
|
|
tbody.appendChild(row);
|
|
} else {
|
|
tbody.insertBefore(row, rows[insertIndex]);
|
|
}
|
|
const detailsRow = document.createElement('tr');
|
|
detailsRow.className = 'calendar-details-row';
|
|
detailsRow.dataset.parentId = entry.id || '';
|
|
detailsRow.dataset.expanded = 'false';
|
|
const actionsHtml = window._calendarIsAdmin ? `
|
|
<div class="calendar-inline-actions">
|
|
<button type="button" class="btn btn-sm btn-outline-secondary calendar-edit" title="Bearbeiten">✎</button>
|
|
<button type="button" class="btn btn-sm btn-outline-danger calendar-delete" title="Löschen">🗑</button>
|
|
</div>
|
|
` : '';
|
|
const detailsText = entry.details ? escapeHtml(entry.details) : '';
|
|
detailsRow.innerHTML = `<td colspan="${window._calendarColSpan || 3}"><div class="calendar-details-text">${detailsText}</div>${actionsHtml}</td>`;
|
|
detailsRow.style.display = 'none';
|
|
row.insertAdjacentElement('afterend', detailsRow);
|
|
applyLocationFilter();
|
|
}
|
|
|
|
async function loadCalendarEntries() {
|
|
const { start, end } = getCalendarRange();
|
|
const todayIso = toLocalISO(start);
|
|
const endIso = toLocalISO(end);
|
|
|
|
clearCalendarEntries();
|
|
resetLocationFilterOptions();
|
|
|
|
try {
|
|
const response = await fetch(`/api/calendar?start=${todayIso}&end=${endIso}`);
|
|
if (!response.ok) throw new Error('Fehler beim Laden');
|
|
const entries = await response.json();
|
|
entries.forEach(entry => {
|
|
addLocationOption(entry.location);
|
|
addCalendarEntry(entry);
|
|
});
|
|
applyLocationFilter();
|
|
} catch (err) {
|
|
console.error('Kalender laden fehlgeschlagen', err);
|
|
}
|
|
}
|
|
|
|
async function saveCalendarEntry(entry) {
|
|
const isUpdate = Boolean(entry.id);
|
|
const url = isUpdate ? `/api/calendar/${entry.id}` : '/api/calendar';
|
|
const method = isUpdate ? 'PUT' : 'POST';
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(entry)
|
|
});
|
|
if (!response.ok) {
|
|
const msg = await response.text();
|
|
throw new Error(msg || 'Speichern fehlgeschlagen');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async function createRecurringEntries(entry, repeatCount) {
|
|
const count = Math.max(1, Math.min(52, repeatCount || 1));
|
|
const baseDate = entry.date;
|
|
if (!baseDate) return [];
|
|
const base = new Date(baseDate);
|
|
const savedEntries = [];
|
|
for (let i = 0; i < count; i++) {
|
|
const next = new Date(base);
|
|
next.setDate(base.getDate() + i * 7);
|
|
const entryForWeek = { ...entry, date: toLocalISO(next) };
|
|
const saved = await saveCalendarEntry(entryForWeek);
|
|
savedEntries.push(saved);
|
|
}
|
|
return savedEntries;
|
|
}
|
|
|
|
async function deleteCalendarEntry(id) {
|
|
const response = await fetch(`/api/calendar/${id}`, { method: 'DELETE' });
|
|
if (!response.ok) {
|
|
const msg = await response.text();
|
|
throw new Error(msg || 'Löschen fehlgeschlagen');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Expose for future dynamic usage
|
|
window.addCalendarEntry = addCalendarEntry;
|
|
window.loadCalendarEntries = loadCalendarEntries;
|
|
window.deleteCalendarEntry = deleteCalendarEntry;
|
|
window.removeCalendarEntryFromUI = removeCalendarEntryFromUI;
|
|
window.applyLocationFilter = applyLocationFilter;
|
|
window.exportCalendar = async function exportCalendar() {
|
|
try {
|
|
const res = await fetch('/api/calendar/export');
|
|
if (!res.ok) throw new Error('Export fehlgeschlagen');
|
|
const blob = await res.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'kalender.xlsx';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
window.URL.revokeObjectURL(url);
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Export fehlgeschlagen.');
|
|
}
|
|
};
|
|
|
|
window.importCalendar = async function importCalendar(file) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const res = await fetch('/api/calendar/import', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
if (!res.ok) {
|
|
const msg = await res.text();
|
|
throw new Error(msg || 'Import fehlgeschlagen');
|
|
}
|
|
return res.json();
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const addBtn = document.getElementById('add-calendar-entry-btn');
|
|
const modalEl = document.getElementById('calendarModal');
|
|
const saveBtn = document.getElementById('saveCalendarEntryBtn');
|
|
const form = document.getElementById('calendarForm');
|
|
const daysContainer = document.getElementById('calendar-days');
|
|
const locationFilter = document.getElementById('calendar-location-filter');
|
|
const repeatCountInput = document.getElementById('calendarRepeatCount');
|
|
const recurrenceRow = document.getElementById('calendarRecurrenceRow');
|
|
const calendarModal = modalEl && window.bootstrap ? new bootstrap.Modal(modalEl) : null;
|
|
if (calendarModal) {
|
|
window.calendarModal = calendarModal;
|
|
}
|
|
let calendarHasLoaded = false;
|
|
|
|
buildCalendarStructure();
|
|
|
|
let lastViewportIsDesktop = isDesktopView();
|
|
let resizeDebounce;
|
|
const handleViewportChange = () => {
|
|
const nowDesktop = isDesktopView();
|
|
if (nowDesktop === lastViewportIsDesktop) return;
|
|
lastViewportIsDesktop = nowDesktop;
|
|
buildCalendarStructure(true);
|
|
if (calendarHasLoaded) {
|
|
loadCalendarEntries();
|
|
}
|
|
};
|
|
window.addEventListener('resize', () => {
|
|
if (resizeDebounce) clearTimeout(resizeDebounce);
|
|
resizeDebounce = setTimeout(handleViewportChange, 150);
|
|
});
|
|
|
|
const setRecurrenceMode = (isEditing) => {
|
|
if (!recurrenceRow) return;
|
|
recurrenceRow.style.display = isEditing ? 'none' : '';
|
|
if (!isEditing && repeatCountInput) {
|
|
repeatCountInput.value = '1';
|
|
}
|
|
};
|
|
|
|
if (addBtn && calendarModal) {
|
|
addBtn.addEventListener('click', () => {
|
|
if (form) form.reset();
|
|
|
|
const today = new Date();
|
|
const dateInput = document.getElementById('calendarDate');
|
|
if (dateInput) dateInput.value = toLocalISO(today);
|
|
|
|
const idInput = document.getElementById('calendarEntryId');
|
|
if (idInput) idInput.value = '';
|
|
|
|
setRecurrenceMode(false);
|
|
calendarModal.show();
|
|
});
|
|
}
|
|
|
|
if (saveBtn && calendarModal) {
|
|
saveBtn.addEventListener('click', async () => {
|
|
if (!form || !form.reportValidity()) return;
|
|
|
|
const entry = {
|
|
id: document.getElementById('calendarEntryId')?.value || '',
|
|
date: document.getElementById('calendarDate')?.value,
|
|
time: document.getElementById('calendarTime')?.value,
|
|
title: document.getElementById('calendarTitle')?.value,
|
|
location: document.getElementById('calendarLocation')?.value,
|
|
details: document.getElementById('calendarDetails')?.value
|
|
};
|
|
const isEditing = Boolean(entry.id);
|
|
const repeatCount = (!isEditing && window._calendarIsAdmin && repeatCountInput)
|
|
? Math.max(1, Math.min(52, parseInt(repeatCountInput.value, 10) || 1))
|
|
: 1;
|
|
|
|
try {
|
|
if (isEditing) {
|
|
removeCalendarEntryFromUI(entry.id);
|
|
const saved = await saveCalendarEntry(entry);
|
|
addLocationOption(saved.location);
|
|
addCalendarEntry(saved);
|
|
} else {
|
|
const savedEntries = await createRecurringEntries(entry, repeatCount);
|
|
savedEntries.forEach(saved => {
|
|
addLocationOption(saved.location);
|
|
addCalendarEntry(saved);
|
|
});
|
|
}
|
|
calendarModal.hide();
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Speichern fehlgeschlagen.');
|
|
}
|
|
});
|
|
}
|
|
|
|
if (window._calendarIsAdmin) {
|
|
const exportBtn = document.getElementById('export-calendar-btn');
|
|
const importBtn = document.getElementById('import-calendar-btn');
|
|
const importInput = document.getElementById('import-calendar-input');
|
|
|
|
if (exportBtn) {
|
|
exportBtn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.exportCalendar();
|
|
});
|
|
}
|
|
|
|
if (importBtn && importInput) {
|
|
importBtn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
importInput.value = '';
|
|
importInput.click();
|
|
});
|
|
|
|
importInput.addEventListener('change', async () => {
|
|
if (!importInput.files || importInput.files.length === 0) return;
|
|
const file = importInput.files[0];
|
|
try {
|
|
await importCalendar(file);
|
|
loadCalendarEntries();
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Import fehlgeschlagen.');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if (daysContainer && typeof admin_enabled !== 'undefined' && admin_enabled) {
|
|
daysContainer.addEventListener('click', async (e) => {
|
|
const editBtn = e.target.closest('.calendar-edit');
|
|
const deleteBtn = e.target.closest('.calendar-delete');
|
|
if (!editBtn && !deleteBtn) return;
|
|
|
|
const row = e.target.closest('tr');
|
|
if (!row) return;
|
|
|
|
let entryRow = row;
|
|
if (row.classList.contains('calendar-details-row') && row.previousElementSibling && row.previousElementSibling.classList.contains('calendar-entry-row')) {
|
|
entryRow = row.previousElementSibling;
|
|
}
|
|
|
|
const entry = {
|
|
id: entryRow.dataset.id,
|
|
date: entryRow.dataset.date,
|
|
time: entryRow.dataset.time,
|
|
title: entryRow.dataset.title,
|
|
location: entryRow.dataset.location,
|
|
details: entryRow.dataset.details
|
|
};
|
|
|
|
if (editBtn) {
|
|
if (form) form.reset();
|
|
document.getElementById('calendarEntryId').value = entry.id || '';
|
|
document.getElementById('calendarDate').value = entry.date || '';
|
|
document.getElementById('calendarTime').value = entry.time || '';
|
|
document.getElementById('calendarTitle').value = entry.title || '';
|
|
document.getElementById('calendarLocation').value = entry.location || '';
|
|
document.getElementById('calendarDetails').value = entry.details || '';
|
|
setRecurrenceMode(true);
|
|
if (calendarModal) calendarModal.show();
|
|
return;
|
|
}
|
|
|
|
if (deleteBtn) {
|
|
if (!entry.id) return;
|
|
const confirmDelete = window.confirm('Diesen Eintrag löschen?');
|
|
if (!confirmDelete) return;
|
|
try {
|
|
await deleteCalendarEntry(entry.id);
|
|
removeCalendarEntryFromUI(entry.id);
|
|
applyLocationFilter();
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert('Löschen fehlgeschlagen.');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
const tabButtons = document.querySelectorAll('.tab-button');
|
|
tabButtons.forEach(btn => {
|
|
if (btn.getAttribute('data-tab') === 'calendar') {
|
|
btn.addEventListener('click', () => {
|
|
if (!calendarHasLoaded) {
|
|
calendarHasLoaded = true;
|
|
loadCalendarEntries();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Immediately load calendar entries for users landing directly on the calendar tab (non-admins may not switch tabs)
|
|
if (!calendarHasLoaded) {
|
|
calendarHasLoaded = true;
|
|
loadCalendarEntries();
|
|
}
|
|
|
|
if (locationFilter) {
|
|
locationFilter.addEventListener('change', () => applyLocationFilter());
|
|
}
|
|
|
|
// Show/hide details inline on row click (any user)
|
|
if (daysContainer) {
|
|
daysContainer.addEventListener('click', (e) => {
|
|
if (e.target.closest('.calendar-edit') || e.target.closest('.calendar-delete')) return;
|
|
const row = e.target.closest('tr.calendar-entry-row');
|
|
if (!row) return;
|
|
const detailsRow = row.nextElementSibling && row.nextElementSibling.classList.contains('calendar-details-row')
|
|
? row.nextElementSibling
|
|
: null;
|
|
if (!detailsRow) return;
|
|
|
|
const isExpanded = detailsRow.dataset.expanded === 'true';
|
|
const shouldShow = !isExpanded;
|
|
detailsRow.dataset.expanded = shouldShow ? 'true' : 'false';
|
|
|
|
// Respect current filter when toggling visibility
|
|
const filterValue = (document.getElementById('calendar-location-filter')?.value || '').trim();
|
|
const rowLoc = (row.dataset.location || '').trim();
|
|
const match = !filterValue || rowLoc === filterValue;
|
|
detailsRow.style.display = (shouldShow && match) ? '' : 'none';
|
|
});
|
|
}
|
|
});
|
|
</script>
|