/** * 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'); // Set initial view mode on calendar grid const grid = $('#calendar-grid'); if (grid) { grid.classList.add('calendar-month-view'); } this.bindEvents(); // Subscribe to store changes store.subscribe('tasks', () => this.render()); store.subscribe('reminders', () => 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') && !e.target.closest('.calendar-week-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`); } // Show/hide weekday headers based on view mode const weekdaysHeader = $('#calendar-weekdays'); if (weekdaysHeader) { weekdaysHeader.style.display = mode === 'month' ? 'grid' : 'none'; } 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; // Ensure calendar grid exists and has correct class const grid = $('#calendar-grid'); if (!grid) return; grid.classList.remove('calendar-month-view', 'calendar-week-view'); grid.classList.add(`calendar-${this.viewMode}-view`); // Ensure weekday headers visibility matches current view mode const weekdaysHeader = $('#calendar-weekdays'); if (weekdaysHeader) { weekdaysHeader.style.display = this.viewMode === 'month' ? 'grid' : 'none'; } this.updateHeader(); // Force a small delay to ensure DOM is ready setTimeout(() => { if (this.viewMode === 'month') { this.renderMonthView(); } else { this.renderWeekView(); } }, 10); } 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) { console.warn('[Calendar] calendar-grid not found for month view'); return; } // Ensure correct classes are set daysContainer.classList.remove('calendar-week-view'); daysContainer.classList.add('calendar-month-view'); 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, weekStart)); } } createWeekDayElement(date, tasksByDate, isToday = false, weekStart = null) { const dateString = this.getDateString(date); const dayTasks = tasksByDate[dateString] || []; const remindersByDate = this.getRemindersByDate(); const dayReminders = remindersByDate[dateString] || []; const hasOverdue = dayTasks.some(t => this.isTaskOverdue(t)); const classes = ['calendar-week-day']; if (isToday) classes.push('today'); if (dayTasks.length > 0 || dayReminders.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' }); // Combined array of reminders and tasks const allItems = []; // Add tasks first (wichtig für kontinuierliche Balken) dayTasks.forEach(task => { allItems.push({ type: 'task', ...task }); }); // Add reminders after tasks dayReminders.forEach(reminder => { allItems.push({ type: 'reminder', id: reminder.id, title: reminder.title, color: reminder.color || '#F59E0B', time: reminder.reminder_time }); }); if (allItems.length === 0) { tasksContainer.appendChild(createElement('div', { className: 'calendar-week-empty' }, ['Keine Aufgaben'])); } else { allItems.forEach(item => { if (item.type === 'reminder') { // Create reminder element const reminderEl = createElement('div', { className: 'calendar-week-task calendar-reminder-item', dataset: { reminderId: item.id }, title: `Erinnerung: ${item.title}${item.time ? ' um ' + item.time : ''}`, onclick: (e) => { e.stopPropagation(); this.editReminder(item.id); } }); // Add title and bell icon reminderEl.appendChild(createElement('span', { className: 'calendar-week-task-title' }, [item.title])); reminderEl.appendChild(createElement('span', { className: 'calendar-reminder-bell' }, ['🔔'])); // Apply color reminderEl.style.backgroundColor = `${item.color}25`; reminderEl.style.borderLeftColor = item.color; tasksContainer.appendChild(reminderEl); } else { // Existing task rendering code const task = item; // 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'); } // Check if this is the first day of the week or the start of the task const isFirstDayOfWeek = weekStart && this.getDateString(date) === this.getDateString(weekStart); const showContent = task.isRangeStart || !task.hasRange || isFirstDayOfWeek; // Build task element children const children = []; // Add title (only at start of task or first day of week) if (showContent) { children.push(createElement('span', { className: 'calendar-week-task-title' }, [task.title])); } // Add user badges if assigned (only at start of task or first day of week, supports multiple) if (userBadge && userBadge.length > 0 && showContent) { 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 remindersByDate = this.getRemindersByDate(); const dayReminders = remindersByDate[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 || dayReminders.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()])); // Combined container for reminders and tasks const allItems = []; // Add tasks first (wichtig für kontinuierliche Balken) dayTasks.forEach(task => { allItems.push({ type: 'task', ...task }); }); // Add reminders after tasks dayReminders.forEach(reminder => { allItems.push({ type: 'reminder', id: reminder.id, title: reminder.title, color: reminder.color || '#F59E0B', time: reminder.reminder_time }); }); // Show max 3 items if (allItems.length > 0) { const tasksContainer = createElement('div', { className: 'calendar-day-tasks' }); allItems.slice(0, 3).forEach(item => { if (item.type === 'reminder') { // Create reminder element const reminderEl = createElement('div', { className: 'calendar-task calendar-reminder-item', dataset: { reminderId: item.id }, title: `Erinnerung: ${item.title}${item.time ? ' um ' + item.time : ''}`, onclick: (e) => { e.stopPropagation(); this.editReminder(item.id); } }); // Add title and bell icon reminderEl.appendChild(createElement('span', { className: 'calendar-task-title' }, [item.title])); reminderEl.appendChild(createElement('span', { className: 'calendar-reminder-bell' }, ['🔔'])); // Apply color reminderEl.style.backgroundColor = `${item.color}40`; reminderEl.style.borderLeftColor = item.color; tasksContainer.appendChild(reminderEl); } else { // Existing task rendering code const task = item; // 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 (allItems.length > 3) { tasksContainer.appendChild(createElement('div', { className: 'calendar-more' }, [`+${allItems.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: Range tasks first (to ensure continuous bars), then single-day tasks filteredTasks.sort((a, b) => { const startA = a.startDate || a.dueDate || ''; const startB = b.startDate || b.dueDate || ''; const endA = a.dueDate || ''; const endB = b.dueDate || ''; // Check if task has range (both start and end date, and they're different) const hasRangeA = a.startDate && a.dueDate && a.startDate !== a.dueDate; const hasRangeB = b.startDate && b.dueDate && b.startDate !== b.dueDate; // Range tasks come first if (hasRangeA && !hasRangeB) return -1; if (!hasRangeA && hasRangeB) return 1; // If both have ranges or both are single-day, sort by start date if (startA !== startB) { return startA.localeCompare(startB); } // Same start date: sort by end date (longer ranges first) if (endA !== endB) { return endB.localeCompare(endA); // Descending to put longer ranges first } // Same dates: 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; } getRemindersByDate() { const reminders = store.get('reminders') || []; if (!Array.isArray(reminders)) { console.warn('[Calendar] Reminders not available or not an array'); return {}; } const activeReminders = reminders.filter(r => r.is_active); const remindersByDate = {}; activeReminders.forEach(reminder => { const date = reminder.reminder_date; if (!remindersByDate[date]) { remindersByDate[date] = []; } remindersByDate[date].push(reminder); }); // Sort reminders by time within each date Object.keys(remindersByDate).forEach(date => { remindersByDate[date].sort((a, b) => (a.reminder_time || '09:00').localeCompare(b.reminder_time || '09:00')); }); return remindersByDate; } 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) { console.log('[Calendar] Day click event:', e.target, 'View mode:', this.viewMode); // Check if clicked on a specific task const taskEl = e.target.closest('.calendar-task, .calendar-week-task'); if (taskEl) { console.log('[Calendar] Task clicked:', taskEl.dataset.taskId); 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')) { console.log('[Calendar] Add task button clicked'); return; // Already handled by onclick } // Show day detail popup for both month and week view if (this.viewMode === 'month') { const dayEl = e.target.closest('.calendar-day'); if (!dayEl) return; console.log('[Calendar] Month day clicked:', dayEl.dataset.date); const dateString = dayEl.dataset.date; this.showDayDetail(dateString, dayEl); } else if (this.viewMode === 'week') { const dayEl = e.target.closest('.calendar-week-day'); console.log('[Calendar] Week day element found:', !!dayEl); if (!dayEl) return; console.log('[Calendar] Week day clicked:', dayEl.dataset.date); const dateString = dayEl.dataset.date; this.showDayDetail(dateString, dayEl); } } showDayDetail(dateString, anchorEl) { console.log('[Calendar] showDayDetail called:', dateString, anchorEl); this.closeDayDetail(); const tasksByDate = this.getTasksByDate(); const dayTasks = tasksByDate[dateString] || []; console.log('[Calendar] Tasks for date:', dayTasks.length); 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'])); // Add reminder button popup.appendChild(createElement('button', { className: 'btn btn-secondary btn-block', style: { marginTop: 'var(--spacing-sm)' }, onclick: () => this.createReminderForDate(dateString) }, ['🔔 Erinnerung hinzufügen'])); // Position popup - different logic for week vs month view const rect = anchorEl.getBoundingClientRect(); let popupTop, popupLeft; if (this.viewMode === 'week') { // For week view, position at the top of the day element popupTop = Math.max(150, rect.top + 50); // Ensure it's visible, minimum 150px from top popupLeft = Math.min(rect.left, window.innerWidth - 350); } else { // For month view, position below the day element popupTop = rect.bottom + 8; popupLeft = Math.min(rect.left, window.innerWidth - 350); } popup.style.top = `${popupTop}px`; popup.style.left = `${popupLeft}px`; console.log('[Calendar] Popup positioning for', this.viewMode, '- Top:', popup.style.top, 'Left:', popup.style.left); document.body.appendChild(popup); this.dayDetailPopup = popup; console.log('[Calendar] Popup created and appended to body'); } 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 } } } })); } createReminderForDate(dateString) { this.closeDayDetail(); window.dispatchEvent(new CustomEvent('modal:open', { detail: { modalId: 'reminder-modal', mode: 'create', data: { prefill: { date: dateString } } } })); } showRemindersForDate(dateString, anchorEl) { const reminders = this.getRemindersByDate(); const dayReminders = reminders[dateString] || []; if (dayReminders.length === 0) return; // Close existing popup this.closeDayDetail(); // Create popup const popup = createElement('div', { className: 'calendar-day-detail calendar-reminder-detail', onclick: (e) => e.stopPropagation() }); this.dayDetailPopup = popup; // Header const header = createElement('h3', {}, [`Erinnerungen für ${formatDate(dateString)}`]); popup.appendChild(header); // Reminders list const remindersList = createElement('div', { className: 'calendar-detail-reminders' }); dayReminders.forEach(reminder => { // Create reminder content (clickable for edit) const reminderContent = createElement('div', { className: 'reminder-content', onclick: () => this.editReminder(reminder.id) }, [ createElement('div', { className: 'reminder-time', style: { color: reminder.color || '#F59E0B' } }, [reminder.reminder_time || '09:00']), createElement('div', { className: 'reminder-title' }, [reminder.title]), reminder.description ? createElement('div', { className: 'reminder-description' }, [reminder.description]) : null ].filter(Boolean)); // Create delete button const deleteBtn = createElement('button', { className: 'reminder-delete-btn', title: 'Erinnerung löschen', onclick: (e) => { e.stopPropagation(); this.deleteReminder(reminder.id, reminder.title); } }, [ createElement('svg', { viewBox: '0 0 24 24', width: '16', height: '16' }, [ createElement('path', { d: 'M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16', stroke: 'currentColor', 'stroke-width': '2', fill: 'none', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }) ]) ]); // Container with content and delete button const reminderItem = createElement('div', { className: 'calendar-detail-reminder' }, [reminderContent, deleteBtn]); remindersList.appendChild(reminderItem); }); popup.appendChild(remindersList); // Add reminder button popup.appendChild(createElement('button', { className: 'btn btn-secondary btn-block', style: { marginTop: 'var(--spacing-md)' }, onclick: () => this.createReminderForDate(dateString) }, ['+ Weitere Erinnerung'])); // Position popup const rect = anchorEl.getBoundingClientRect(); let popupTop = rect.bottom + 8; let popupLeft = Math.min(rect.left, window.innerWidth - 350); popup.style.top = `${popupTop}px`; popup.style.left = `${popupLeft}px`; document.body.appendChild(popup); } editReminder(reminderId) { this.closeDayDetail(); window.dispatchEvent(new CustomEvent('modal:open', { detail: { modalId: 'reminder-modal', mode: 'edit', data: { reminderId } } })); } async deleteReminder(reminderId, reminderTitle) { if (!confirm(`Erinnerung "${reminderTitle}" wirklich löschen?`)) { return; } try { // Import reminder manager if needed if (typeof reminderManager !== 'undefined') { await reminderManager.deleteReminder(reminderId); } else { // Fallback direct API call await api.request(`/reminders/${reminderId}`, { method: 'DELETE' }); // Update store const reminders = store.get('reminders') || []; const updatedReminders = reminders.filter(r => r.id !== reminderId); store.setReminders(updatedReminders); } // Close popup and refresh calendar this.closeDayDetail(); this.render(); console.log(`[Calendar] Reminder ${reminderId} deleted`); } catch (error) { console.error('[Calendar] Failed to delete reminder:', error); alert('Fehler beim Löschen der Erinnerung. Bitte versuche es erneut.'); } } // ===================== // 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;