/** * TASKMATE - Calendar View Module * ================================ * Calendar view for tasks with week and month views */ import store from './store.js'; import { $, $$, createElement, clearElement, formatDate, getDueDateStatus, filterTasks, createPriorityElement } from './utils.js'; class CalendarViewManager { constructor() { this.container = null; this.currentDate = new Date(); this.selectedDate = null; this.dayDetailPopup = null; this.viewMode = 'month'; // 'month' or 'week' // Dynamic filter states - keys are filter category names, values are booleans this.enabledFilters = {}; this.filterMenuOpen = false; // Calendar-specific search this.highlightedTaskId = null; this.searchQuery = ''; this.init(); } init() { this.container = $('#view-calendar'); this.bindEvents(); // Subscribe to store changes store.subscribe('tasks', () => this.render()); store.subscribe('filters', (filters) => { // Calendar-specific search behavior if (store.get('currentView') === 'calendar') { this.handleCalendarSearch(filters.search || ''); } }); store.subscribe('searchResultIds', () => this.render()); store.subscribe('currentView', (view) => { if (view === 'calendar') { // Re-apply search when switching to calendar view const currentSearch = store.get('filters')?.search || ''; if (currentSearch) { this.handleCalendarSearch(currentSearch); } else { this.render(); } } }); } bindEvents() { // Navigation buttons $('#btn-prev-period')?.addEventListener('click', () => this.navigate(-1)); $('#btn-next-period')?.addEventListener('click', () => this.navigate(1)); $('#btn-today')?.addEventListener('click', () => this.goToToday()); // View toggle buttons $$('[data-calendar-view]').forEach(btn => { btn.addEventListener('click', (e) => { const newView = e.target.dataset.calendarView; this.setViewMode(newView); }); }); // Filter dropdown toggle $('#btn-calendar-filter')?.addEventListener('click', (e) => { e.stopPropagation(); this.toggleFilterMenu(); }); // Close filter menu when clicking outside document.addEventListener('click', (e) => { if (this.filterMenuOpen && !e.target.closest('.calendar-filter-dropdown')) { this.closeFilterMenu(); } }); // Subscribe to columns changes to update filters and colors store.subscribe('columns', () => { this.renderFilterMenu(); this.render(); // Update task colors when column colors change }); // Calendar day clicks $('#calendar-grid')?.addEventListener('click', (e) => this.handleDayClick(e)); // Close popup on outside click document.addEventListener('click', (e) => { if (this.dayDetailPopup && !this.dayDetailPopup.contains(e.target) && !e.target.closest('.calendar-day')) { this.closeDayDetail(); } }); } // ===================== // VIEW MODE // ===================== setViewMode(mode) { this.viewMode = mode; // Update toggle buttons $$('[data-calendar-view]').forEach(btn => { btn.classList.toggle('active', btn.dataset.calendarView === mode); }); // Update grid class const grid = $('#calendar-grid'); if (grid) { grid.classList.remove('calendar-month-view', 'calendar-week-view'); grid.classList.add(`calendar-${mode}-view`); } this.render(); } // ===================== // NAVIGATION // ===================== navigate(delta) { if (this.viewMode === 'month') { this.currentDate = new Date( this.currentDate.getFullYear(), this.currentDate.getMonth() + delta, 1 ); } else { // Week view - navigate by 7 days const newDate = new Date(this.currentDate); newDate.setDate(newDate.getDate() + (delta * 7)); this.currentDate = newDate; } this.render(); } goToToday() { this.currentDate = new Date(); this.highlightedTaskId = null; this.searchQuery = ''; this.render(); } // ===================== // CALENDAR SEARCH // ===================== handleCalendarSearch(query) { this.searchQuery = query.trim().toLowerCase(); if (!this.searchQuery) { // Clear search - reset highlight and just re-render this.highlightedTaskId = null; this.render(); return; } // Find tasks matching the title const tasks = store.get('tasks').filter(t => !t.archived); const matchingTasks = tasks.filter(t => t.title && t.title.toLowerCase().includes(this.searchQuery) ); if (matchingTasks.length === 0) { // No matches - clear highlight this.highlightedTaskId = null; this.render(); return; } // Find the first task with a start date or due date const taskWithDate = matchingTasks.find(t => t.startDate || t.dueDate); if (taskWithDate) { this.highlightedTaskId = taskWithDate.id; // Navigate to the task's start date (or due date if no start) const targetDate = taskWithDate.startDate || taskWithDate.dueDate; if (targetDate) { this.navigateToDate(targetDate); } } else { // Highlight first match even if no date this.highlightedTaskId = matchingTasks[0].id; this.render(); } } navigateToDate(dateString) { const date = new Date(dateString); // Set current date to the month/week containing this date this.currentDate = new Date(date.getFullYear(), date.getMonth(), 1); // For week view, adjust to the week containing the date if (this.viewMode === 'week') { this.currentDate = this.getWeekStart(date); } this.render(); // Scroll the highlighted task into view after render setTimeout(() => { const highlightedEl = $('.calendar-task.search-highlight, .calendar-week-task.search-highlight'); if (highlightedEl) { highlightedEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, 100); } clearSearch() { this.highlightedTaskId = null; this.searchQuery = ''; this.render(); } // ===================== // RENDERING // ===================== render() { if (store.get('currentView') !== 'calendar') return; this.updateHeader(); if (this.viewMode === 'month') { this.renderMonthView(); } else { this.renderWeekView(); } } updateHeader() { const titleEl = $('#calendar-title'); if (!titleEl) return; if (this.viewMode === 'month') { const options = { month: 'long', year: 'numeric' }; titleEl.textContent = this.currentDate.toLocaleDateString('de-DE', options); } else { // Week view - show week range const weekStart = this.getWeekStart(this.currentDate); const weekEnd = new Date(weekStart); weekEnd.setDate(weekEnd.getDate() + 6); const startStr = weekStart.toLocaleDateString('de-DE', { day: 'numeric', month: 'short' }); const endStr = weekEnd.toLocaleDateString('de-DE', { day: 'numeric', month: 'short', year: 'numeric' }); titleEl.textContent = `${startStr} - ${endStr}`; } } // ===================== // MONTH VIEW // ===================== renderMonthView() { const daysContainer = $('#calendar-grid'); if (!daysContainer) return; clearElement(daysContainer); const year = this.currentDate.getFullYear(); const month = this.currentDate.getMonth(); // Get first day of month and how many days in month const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const daysInMonth = lastDay.getDate(); // Get the starting day of week (0 = Sunday, adjust for Monday start) let startingDay = firstDay.getDay(); startingDay = startingDay === 0 ? 6 : startingDay - 1; // Monday = 0 // Get tasks by date const tasksByDate = this.getTasksByDate(); // Today for comparison const today = new Date(); const todayString = this.getDateString(today); // Fill in days from previous month const prevMonthLastDay = new Date(year, month, 0).getDate(); for (let i = startingDay - 1; i >= 0; i--) { const day = prevMonthLastDay - i; const date = new Date(year, month - 1, day); daysContainer.appendChild(this.createDayElement(date, day, true, tasksByDate)); } // Fill in days of current month for (let day = 1; day <= daysInMonth; day++) { const date = new Date(year, month, day); const dateString = this.getDateString(date); const isToday = dateString === todayString; daysContainer.appendChild(this.createDayElement(date, day, false, tasksByDate, isToday)); } // Fill in days from next month const totalCells = startingDay + daysInMonth; const remainingCells = totalCells % 7 === 0 ? 0 : 7 - (totalCells % 7); for (let day = 1; day <= remainingCells; day++) { const date = new Date(year, month + 1, day); daysContainer.appendChild(this.createDayElement(date, day, true, tasksByDate)); } } // ===================== // WEEK VIEW // ===================== renderWeekView() { const daysContainer = $('#calendar-grid'); if (!daysContainer) return; clearElement(daysContainer); const weekStart = this.getWeekStart(this.currentDate); const tasksByDate = this.getTasksByDate(); const today = new Date(); const todayString = this.getDateString(today); // Create 7 days for the week for (let i = 0; i < 7; i++) { const date = new Date(weekStart); date.setDate(weekStart.getDate() + i); const dateString = this.getDateString(date); const isToday = dateString === todayString; daysContainer.appendChild(this.createWeekDayElement(date, tasksByDate, isToday)); } } createWeekDayElement(date, tasksByDate, isToday = false) { const dateString = this.getDateString(date); const dayTasks = tasksByDate[dateString] || []; const hasOverdue = dayTasks.some(t => this.isTaskOverdue(t)); const classes = ['calendar-week-day']; if (isToday) classes.push('today'); if (dayTasks.length > 0) classes.push('has-tasks'); if (hasOverdue) classes.push('has-overdue'); const dayEl = createElement('div', { className: classes.join(' '), dataset: { date: dateString } }); // Day header with date const dayHeader = createElement('div', { className: 'calendar-week-day-header' }, [ createElement('span', { className: 'calendar-week-day-name' }, [ date.toLocaleDateString('de-DE', { weekday: 'short' }) ]), createElement('span', { className: `calendar-week-day-number ${isToday ? 'today' : ''}` }, [ date.getDate().toString() ]) ]); dayEl.appendChild(dayHeader); // Tasks container const tasksContainer = createElement('div', { className: 'calendar-week-day-tasks' }); if (dayTasks.length === 0) { tasksContainer.appendChild(createElement('div', { className: 'calendar-week-empty' }, ['Keine Aufgaben'])); } else { dayTasks.forEach(task => { // Get column color and user badge info const columnColor = this.getColumnColor(task); const userBadge = this.getUserBadgeInfo(task); // Build class list const taskClasses = ['calendar-week-task']; if (this.isTaskOverdue(task)) taskClasses.push('overdue'); if (task.hasRange) { taskClasses.push('has-range'); if (task.isRangeStart) taskClasses.push('range-start'); if (task.isRangeMiddle) taskClasses.push('range-middle'); if (task.isRangeEnd) taskClasses.push('range-end'); } // Add search highlight if (this.highlightedTaskId === task.id) { taskClasses.push('search-highlight'); } // Build task element children const children = [ createElement('span', { className: 'calendar-week-task-title' }, [task.title]) ]; // Add user badges if assigned (supports multiple) if (userBadge && userBadge.length > 0) { const badgeContainer = createElement('span', { className: 'calendar-task-badges' }); userBadge.forEach(badge => { const badgeEl = createElement('span', { className: 'calendar-task-user-badge' }, [badge.initials]); badgeEl.style.backgroundColor = badge.color; badgeContainer.appendChild(badgeEl); }); children.push(badgeContainer); } const taskEl = createElement('div', { className: taskClasses.join(' '), dataset: { taskId: task.id }, title: this.getTaskTooltip(task) }, children); // Apply column color taskEl.style.backgroundColor = `${columnColor}25`; if (task.isRangeStart || !task.hasRange) { taskEl.style.borderLeftColor = columnColor; } tasksContainer.appendChild(taskEl); }); } dayEl.appendChild(tasksContainer); // Add task button const addBtn = createElement('button', { className: 'calendar-week-add-task', onclick: (e) => { e.stopPropagation(); this.createTaskForDate(dateString); } }, ['+ Aufgabe']); dayEl.appendChild(addBtn); return dayEl; } getWeekStart(date) { const d = new Date(date); const day = d.getDay(); const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Adjust for Monday start return new Date(d.setDate(diff)); } // ===================== // MONTH VIEW DAY ELEMENT // ===================== createDayElement(date, dayNumber, isOtherMonth, tasksByDate, isToday = false) { const dateString = this.getDateString(date); const dayTasks = tasksByDate[dateString] || []; const hasOverdue = dayTasks.some(t => this.isTaskOverdue(t)); const classes = ['calendar-day']; if (isOtherMonth) classes.push('other-month'); if (isToday) classes.push('today'); if (dayTasks.length > 0) classes.push('has-tasks'); if (hasOverdue) classes.push('has-overdue'); const dayEl = createElement('div', { className: classes.join(' '), dataset: { date: dateString } }); // Day number dayEl.appendChild(createElement('span', { className: 'calendar-day-number' }, [dayNumber.toString()])); // Tasks preview (show max 3) if (dayTasks.length > 0) { const tasksContainer = createElement('div', { className: 'calendar-day-tasks' }); dayTasks.slice(0, 3).forEach(task => { // Get column color and user badge info const columnColor = this.getColumnColor(task); const userBadge = this.getUserBadgeInfo(task); // Build class list const taskClasses = ['calendar-task']; if (this.isTaskOverdue(task)) taskClasses.push('overdue'); if (task.hasRange) { taskClasses.push('has-range'); if (task.isRangeStart) taskClasses.push('range-start'); if (task.isRangeMiddle) taskClasses.push('range-middle'); if (task.isRangeEnd) taskClasses.push('range-end'); } // Add search highlight if (this.highlightedTaskId === task.id) { taskClasses.push('search-highlight'); } // Build content - show title and optional user badge const taskEl = createElement('div', { className: taskClasses.join(' '), dataset: { taskId: task.id }, title: this.getTaskTooltip(task) }); // Add title (not for middle parts of range) if (!task.isRangeMiddle) { taskEl.appendChild(createElement('span', { className: 'calendar-task-title' }, [task.title])); } // Add user badges if assigned (only on start or single-day tasks, supports multiple) if (userBadge && userBadge.length > 0 && (task.isRangeStart || !task.hasRange)) { const badgeContainer = createElement('span', { className: 'calendar-task-badges' }); userBadge.forEach(badge => { const badgeEl = createElement('span', { className: 'calendar-task-user-badge' }, [badge.initials]); badgeEl.style.backgroundColor = badge.color; badgeContainer.appendChild(badgeEl); }); taskEl.appendChild(badgeContainer); } // Apply column color taskEl.style.backgroundColor = `${columnColor}40`; if (task.isRangeStart || !task.hasRange) { taskEl.style.borderLeftColor = columnColor; } tasksContainer.appendChild(taskEl); }); if (dayTasks.length > 3) { tasksContainer.appendChild(createElement('div', { className: 'calendar-more' }, [`+${dayTasks.length - 3} weitere`])); } dayEl.appendChild(tasksContainer); } return dayEl; } // ===================== // DATA // ===================== getTasksByDate() { const tasks = store.get('tasks').filter(t => !t.archived && (t.dueDate || t.startDate)); const filters = store.get('filters'); const columns = store.get('columns'); // Calendar uses its own search logic, so ignore the general search filter const filtersWithoutSearch = { ...filters, search: '', dueDate: 'all' }; let filteredTasks = filterTasks(tasks, filtersWithoutSearch, [], columns); // Ignore search and due date filter for calendar // Filter tasks based on filter category checkboxes // EXCEPTION: During active search, show all tasks (ignore checkbox filters) const isSearchActive = this.searchQuery && this.searchQuery.length > 0; if (columns.length > 0 && !isSearchActive) { // Build a map of columnId -> filterCategory const columnFilterMap = {}; columns.forEach(col => { columnFilterMap[col.id] = col.filterCategory || 'in_progress'; }); filteredTasks = filteredTasks.filter(task => { const filterCategory = columnFilterMap[task.columnId]; // If no filter state exists for this category, default to showing "in_progress" if (this.enabledFilters[filterCategory] === undefined) { // Default: show in_progress, hide open and completed return filterCategory === 'in_progress'; } return this.enabledFilters[filterCategory]; }); } // Sort tasks: earliest start date first (so they stay on top throughout their duration), // then alphabetically by title for same start date filteredTasks.sort((a, b) => { const startA = a.startDate || a.dueDate || ''; const startB = b.startDate || b.dueDate || ''; // Compare start dates (earliest first = ascending) if (startA !== startB) { return startA.localeCompare(startB); } // Same start date: sort alphabetically by title return (a.title || '').localeCompare(b.title || '', 'de'); }); const tasksByDate = {}; // Process tasks in sorted order to maintain consistent positioning across all days filteredTasks.forEach(task => { const startDateStr = task.startDate ? task.startDate.split('T')[0] : null; const endDateStr = task.dueDate ? task.dueDate.split('T')[0] : null; // If task has both start and end date, add to all days in range if (startDateStr && endDateStr) { const start = new Date(startDateStr); const end = new Date(endDateStr); const current = new Date(start); while (current <= end) { const dateKey = this.getDateString(current); if (!tasksByDate[dateKey]) { tasksByDate[dateKey] = []; } // Add task with position info for multi-day display const taskWithRange = { ...task, isRangeStart: dateKey === startDateStr, isRangeEnd: dateKey === endDateStr, isRangeMiddle: dateKey !== startDateStr && dateKey !== endDateStr, hasRange: true }; tasksByDate[dateKey].push(taskWithRange); current.setDate(current.getDate() + 1); } } else { // Single date task (either start or end) const dateKey = endDateStr || startDateStr; if (dateKey) { if (!tasksByDate[dateKey]) { tasksByDate[dateKey] = []; } tasksByDate[dateKey].push({ ...task, hasRange: false }); } } }); return tasksByDate; } getDateString(date) { // Use local date components instead of toISOString() which converts to UTC // This fixes the issue where the date is off by one day due to timezone differences const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } // ===================== // DAY DETAIL POPUP // ===================== handleDayClick(e) { // Check if clicked on a specific task const taskEl = e.target.closest('.calendar-task, .calendar-week-task'); if (taskEl) { const taskId = parseInt(taskEl.dataset.taskId); this.openTaskModal(taskId); return; } // Check if clicked on add button (week view) if (e.target.closest('.calendar-week-add-task')) { return; // Already handled by onclick } // For month view, show day detail popup if (this.viewMode === 'month') { const dayEl = e.target.closest('.calendar-day'); if (!dayEl) return; const dateString = dayEl.dataset.date; this.showDayDetail(dateString, dayEl); } } showDayDetail(dateString, anchorEl) { this.closeDayDetail(); const tasksByDate = this.getTasksByDate(); const dayTasks = tasksByDate[dateString] || []; const date = new Date(dateString); const dateDisplay = date.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); const popup = createElement('div', { className: 'calendar-day-detail' }); // Header const header = createElement('div', { className: 'calendar-day-detail-header' }, [ createElement('span', { className: 'calendar-day-detail-date' }, [dateDisplay]), createElement('button', { className: 'btn btn-icon btn-ghost', onclick: () => this.closeDayDetail() }, ['×']) ]); popup.appendChild(header); // Tasks list const tasksList = createElement('div', { className: 'calendar-day-detail-tasks' }); if (dayTasks.length === 0) { tasksList.appendChild(createElement('p', { className: 'text-secondary', style: { textAlign: 'center', padding: 'var(--spacing-md)' } }, ['Keine Aufgaben'])); } else { dayTasks.forEach(task => { const taskItem = createElement('div', { className: 'calendar-detail-task', dataset: { taskId: task.id }, onclick: () => this.openTaskModal(task.id) }, [ createPriorityElement(task.priority), createElement('span', { className: 'calendar-detail-title' }, [task.title]) ]); tasksList.appendChild(taskItem); }); } popup.appendChild(tasksList); // Add task button popup.appendChild(createElement('button', { className: 'btn btn-primary btn-block', style: { marginTop: 'var(--spacing-md)' }, onclick: () => this.createTaskForDate(dateString) }, ['+ Aufgabe hinzufügen'])); // Position popup const rect = anchorEl.getBoundingClientRect(); popup.style.top = `${rect.bottom + 8}px`; popup.style.left = `${Math.min(rect.left, window.innerWidth - 350)}px`; document.body.appendChild(popup); this.dayDetailPopup = popup; } closeDayDetail() { if (this.dayDetailPopup) { this.dayDetailPopup.remove(); this.dayDetailPopup = null; } } // ===================== // ACTIONS // ===================== openTaskModal(taskId) { this.closeDayDetail(); window.dispatchEvent(new CustomEvent('modal:open', { detail: { modalId: 'task-modal', mode: 'edit', data: { taskId } } })); } createTaskForDate(dateString) { this.closeDayDetail(); const columns = store.get('columns'); const firstColumn = columns[0]; window.dispatchEvent(new CustomEvent('modal:open', { detail: { modalId: 'task-modal', mode: 'create', data: { columnId: firstColumn?.id, prefill: { dueDate: dateString } } } })); } // ===================== // HELPERS // ===================== getTaskTooltip(task) { const formatDateGerman = (dateStr) => { if (!dateStr) return null; const date = new Date(dateStr); return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); }; const startDate = formatDateGerman(task.startDate); const endDate = formatDateGerman(task.dueDate); // Get assigned user names const users = store.get('users'); let assignedNames = []; // Neue Mehrfachzuweisung if (task.assignees && task.assignees.length > 0) { task.assignees.forEach(assignee => { const currentUser = users.find(u => u.id === assignee.id); const name = currentUser?.display_name || assignee.display_name || assignee.username || '??'; assignedNames.push(name); }); } else if (task.assignedTo) { // Fallback: Alte Einzelzuweisung const user = users.find(u => u.id === task.assignedTo); if (user) { assignedNames.push(user.display_name || user.username); } } let tooltip = task.title; if (startDate && endDate) { tooltip += `\n(${startDate} - ${endDate})`; } else if (endDate) { tooltip += `\n(Fällig: ${endDate})`; } else if (startDate) { tooltip += `\n(Start: ${startDate})`; } if (assignedNames.length > 0) { tooltip += `\nZugewiesen: ${assignedNames.join(', ')}`; } return tooltip; } getPriorityColor(priority) { const colors = { high: 'var(--priority-high)', medium: 'var(--priority-medium)', low: 'var(--priority-low)' }; return colors[priority] || colors.medium; } getTaskUserColor(task) { const users = store.get('users'); const hgUser = users.find(u => u.username === 'HG'); const mhUser = users.find(u => u.username === 'MH'); // Check if task is assigned to a user if (task.assignedTo) { const assignedUser = users.find(u => u.id === task.assignedTo); if (assignedUser) { return assignedUser.color; } } // Default: no specific color (use neutral) return null; } getColumnColor(task) { const columns = store.get('columns'); const column = columns.find(c => c.id === task.columnId); return column?.color || '#6B7280'; // Default gray if no color set } getUserBadgeInfo(task) { const users = store.get('users'); const badges = []; // Neue Mehrfachzuweisung: task.assignees Array if (task.assignees && task.assignees.length > 0) { task.assignees.forEach(assignee => { const currentUser = users.find(u => u.id === assignee.id); const initials = currentUser?.initials || assignee.initials || '??'; const color = currentUser?.color || assignee.color || '#6B7280'; badges.push({ initials, color }); }); return badges.length > 0 ? badges : null; } // Fallback: Alte Einzelzuweisung if (task.assignedTo) { const user = users.find(u => u.id === task.assignedTo); if (user) { const initials = user.initials || '??'; badges.push({ initials, color: user.color || '#6B7280' }); return badges; } } return null; } // Check if task is in the last column (completed) isTaskCompleted(task) { const columns = store.get('columns'); if (!columns || columns.length === 0) return false; const lastColumnId = columns[columns.length - 1].id; return task.columnId === lastColumnId; } // Check if task should show overdue status (not completed and overdue) isTaskOverdue(task) { if (this.isTaskCompleted(task)) return false; return getDueDateStatus(task.dueDate) === 'overdue'; } // ===================== // FILTER MENU // ===================== toggleFilterMenu() { if (this.filterMenuOpen) { this.closeFilterMenu(); } else { this.openFilterMenu(); } } openFilterMenu() { this.filterMenuOpen = true; this.renderFilterMenu(); $('#calendar-filter-menu')?.classList.remove('hidden'); } closeFilterMenu() { this.filterMenuOpen = false; $('#calendar-filter-menu')?.classList.add('hidden'); } getUniqueFilterCategories() { const columns = store.get('columns') || []; const categories = new Map(); // Standard-Kategorien mit deutschen Labels const defaultLabels = { 'open': 'Offen', 'in_progress': 'In Arbeit', 'completed': 'Erledigt' }; columns.forEach(col => { const category = col.filterCategory || 'in_progress'; if (!categories.has(category)) { // Für Standard-Kategorien deutschen Label verwenden, sonst den Kategorienamen selbst const label = defaultLabels[category] || category; categories.set(category, label); } }); // Stelle sicher, dass die Standard-Kategorien immer vorhanden sind (in der richtigen Reihenfolge) const result = []; if (categories.has('open')) { result.push({ key: 'open', label: categories.get('open') }); categories.delete('open'); } if (categories.has('in_progress')) { result.push({ key: 'in_progress', label: categories.get('in_progress') }); categories.delete('in_progress'); } if (categories.has('completed')) { result.push({ key: 'completed', label: categories.get('completed') }); categories.delete('completed'); } // Benutzerdefinierte Kategorien hinzufügen categories.forEach((label, key) => { result.push({ key, label }); }); return result; } renderFilterMenu() { const menu = $('#calendar-filter-menu'); if (!menu) return; const categories = this.getUniqueFilterCategories(); // Default: in_progress aktiviert, wenn noch keine Filter gesetzt if (Object.keys(this.enabledFilters).length === 0) { this.enabledFilters['in_progress'] = true; } menu.innerHTML = categories.map(cat => { const isChecked = this.enabledFilters[cat.key] === true; return ` `; }).join(''); // Event-Listener für Checkboxen menu.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { checkbox.addEventListener('change', (e) => { const category = e.target.dataset.filterCategory; this.enabledFilters[category] = e.target.checked; e.target.closest('.calendar-filter-item').classList.toggle('checked', e.target.checked); this.render(); }); }); } } // Create and export singleton const calendarViewManager = new CalendarViewManager(); export default calendarViewManager;