1007 Zeilen
31 KiB
JavaScript
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?.username || assignee.username || '??';
|
|
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.username || '??';
|
|
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;
|