Files
TaskMate/frontend/js/calendar.js
hendrik_gebhardt@gmx.de 623bbdf5dd Gitea-Repo fix
2026-01-04 21:21:11 +00:00

1007 Zeilen
31 KiB
JavaScript

/**
* 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 `
<label class="calendar-filter-item ${isChecked ? 'checked' : ''}">
<input type="checkbox" data-filter-category="${cat.key}" ${isChecked ? 'checked' : ''}>
<span>${cat.label}</span>
</label>
`;
}).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;