Dieser Commit ist enthalten in:
hendrik_gebhardt@gmx.de
2026-01-06 21:49:26 +00:00
committet von Server Deploy
Ursprung 623bbdf5dd
Commit 7d67557be4
34 geänderte Dateien mit 21416 neuen und 2367 gelöschten Zeilen

Datei anzeigen

@ -32,10 +32,17 @@ class CalendarViewManager {
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') {
@ -95,7 +102,7 @@ class CalendarViewManager {
// 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-day') && !e.target.closest('.calendar-week-day')) {
this.closeDayDetail();
}
});
@ -120,6 +127,12 @@ class CalendarViewManager {
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();
}
@ -230,13 +243,29 @@ class CalendarViewManager {
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();
if (this.viewMode === 'month') {
this.renderMonthView();
} else {
this.renderWeekView();
}
// Force a small delay to ensure DOM is ready
setTimeout(() => {
if (this.viewMode === 'month') {
this.renderMonthView();
} else {
this.renderWeekView();
}
}, 10);
}
updateHeader() {
@ -264,7 +293,14 @@ class CalendarViewManager {
renderMonthView() {
const daysContainer = $('#calendar-grid');
if (!daysContainer) return;
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);
@ -334,18 +370,20 @@ class CalendarViewManager {
const dateString = this.getDateString(date);
const isToday = dateString === todayString;
daysContainer.appendChild(this.createWeekDayElement(date, tasksByDate, isToday));
daysContainer.appendChild(this.createWeekDayElement(date, tasksByDate, isToday, weekStart));
}
}
createWeekDayElement(date, tasksByDate, isToday = false) {
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) classes.push('has-tasks');
if (dayTasks.length > 0 || dayReminders.length > 0) classes.push('has-tasks');
if (hasOverdue) classes.push('has-overdue');
const dayEl = createElement('div', {
@ -367,12 +405,58 @@ class CalendarViewManager {
// Tasks container
const tasksContainer = createElement('div', { className: 'calendar-week-day-tasks' });
if (dayTasks.length === 0) {
// 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 {
dayTasks.forEach(task => {
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);
@ -391,13 +475,20 @@ class CalendarViewManager {
taskClasses.push('search-highlight');
}
// Build task element children
const children = [
createElement('span', { className: 'calendar-week-task-title' }, [task.title])
];
// 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;
// Add user badges if assigned (supports multiple)
if (userBadge && userBadge.length > 0) {
// 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]);
@ -420,6 +511,7 @@ class CalendarViewManager {
}
tasksContainer.appendChild(taskEl);
}
});
}
@ -452,12 +544,14 @@ class CalendarViewManager {
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) classes.push('has-tasks');
if (dayTasks.length > 0 || dayReminders.length > 0) classes.push('has-tasks');
if (hasOverdue) classes.push('has-overdue');
const dayEl = createElement('div', {
@ -470,11 +564,57 @@ class CalendarViewManager {
className: 'calendar-day-number'
}, [dayNumber.toString()]));
// Tasks preview (show max 3)
if (dayTasks.length > 0) {
// 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' });
dayTasks.slice(0, 3).forEach(task => {
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);
@ -523,12 +663,13 @@ class CalendarViewManager {
}
tasksContainer.appendChild(taskEl);
}
});
if (dayTasks.length > 3) {
if (allItems.length > 3) {
tasksContainer.appendChild(createElement('div', {
className: 'calendar-more'
}, [`+${dayTasks.length - 3} weitere`]));
}, [`+${allItems.length - 3} weitere`]));
}
dayEl.appendChild(tasksContainer);
@ -570,18 +711,32 @@ class CalendarViewManager {
});
}
// Sort tasks: earliest start date first (so they stay on top throughout their duration),
// then alphabetically by title for same start date
// 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 || '';
// Compare start dates (earliest first = ascending)
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 start date: sort alphabetically by title
// Same dates: sort alphabetically by title
return (a.title || '').localeCompare(b.title || '', 'de');
});
@ -629,6 +784,32 @@ class CalendarViewManager {
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
@ -643,9 +824,12 @@ class CalendarViewManager {
// =====================
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;
@ -653,24 +837,36 @@ class CalendarViewManager {
// 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
}
// For month view, show day detail popup
// 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', {
@ -726,13 +922,35 @@ class CalendarViewManager {
onclick: () => this.createTaskForDate(dateString)
}, ['+ Aufgabe hinzufügen']));
// Position popup
// 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();
popup.style.top = `${rect.bottom + 8}px`;
popup.style.left = `${Math.min(rect.left, window.innerWidth - 350)}px`;
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() {
@ -778,6 +996,155 @@ class CalendarViewManager {
}));
}
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
// =====================