document.addEventListener('DOMContentLoaded', fetchHabits); const habitListDiv = document.getElementById('habitList'); const dateModal = document.getElementById('dateModal'); const modalHabitName = document.getElementById('modalHabitName'); const modalDateGrid = document.getElementById('modalDateGrid'); const modalCompleteTodayBtn = document.getElementById('modalCompleteTodayBtn'); const modalEditBtn = document.getElementById('modalEditBtn'); // Neu hinzugefügt const modalDeleteBtn = document.getElementById('modalDeleteBtn'); // Neu hinzugefügt let currentHabitId = null; // Stellt sicher, dass diese globale Variable korrekt ist let currentModalDate = new Date(); // Aktueller Monat im Modal let currentHabitCompletedDates = {}; // Completed dates für das aktuelle Habit (now object with counts) let currentHabitColor = '#4CAF50'; // Current habit color let currentHabitDailyTarget = 1; // Current habit daily target function getCurrentDate() { const today = new Date(); const year = today.getFullYear(); const month = String(today.getMonth() + 1).padStart(2, '0'); const day = String(today.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } function getPastDates(days) { const dates = []; for (let i = days - 1; i >= 0; i--) { const d = new Date(); d.setDate(d.getDate() - i); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); dates.push(`${year}-${month}-${day}`); } return dates; } function calculateStreak(completedDates, dailyTarget) { if (!completedDates || Object.keys(completedDates).length === 0) { return 0; } const today = getCurrentDate(); let streak = 0; let currentDate = new Date(); // Check if today is completed, if not, start from yesterday const todayCount = completedDates[today] || 0; if (todayCount < dailyTarget) { currentDate.setDate(currentDate.getDate() - 1); } // Count consecutive days backwards from today (or yesterday) while (true) { const dateStr = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(currentDate.getDate()).padStart(2, '0')}`; const count = completedDates[dateStr] || 0; if (count >= dailyTarget) { streak++; currentDate.setDate(currentDate.getDate() - 1); } else { break; } } return streak; } async function fetchHabits() { try { const response = await fetch('/habits'); const habits = await response.json(); renderHabits(habits); } catch (error) { console.error('Error fetching habits:', error); } } function renderHabits(habits) { habitListDiv.innerHTML = ''; // Clear previous habits const last30Days = getPastDates(30); habits.forEach(habit => { const habitItem = document.createElement('div'); habitItem.className = 'habit-item'; habitItem.dataset.id = habit.id; // Store habit ID // Apply custom color to the habit item const habitColor = habit.color || '#4CAF50'; habitItem.style.setProperty('--habit-color', habitColor); const habitHeader = document.createElement('div'); habitHeader.className = 'habit-header'; const habitName = document.createElement('div'); habitName.className = 'habit-name'; habitName.textContent = habit.name; habitHeader.appendChild(habitName); const habitActions = document.createElement('div'); habitActions.className = 'habit-actions'; // Haken-Button für heute abhaken/rückgängig machen mit Zähler const todayToggleButton = document.createElement('button'); const today = getCurrentDate(); const todayCount = habit.completed_dates[today] || 0; const dailyTarget = habit.daily_target || 1; const isCompletedToday = todayCount >= dailyTarget; // Show count if target > 1, otherwise just checkmark if (dailyTarget > 1) { todayToggleButton.innerHTML = `${todayCount}/${dailyTarget}`; } else { todayToggleButton.innerHTML = '✓'; // Checkmark symbol } todayToggleButton.className = isCompletedToday ? 'completed-today' : 'not-completed-today'; todayToggleButton.onclick = () => toggleTodayCompletion(habit.id, today, todayToggleButton, habit.daily_target); habitActions.appendChild(todayToggleButton); // Drei-Punkte-Menü für erweiterte Optionen const menuButton = document.createElement('button'); menuButton.innerHTML = '⋮'; // Vertical ellipsis (drei Punkte) menuButton.onclick = () => openHabitModal(habit.id, habit.name, habit.completed_dates, habitColor, habit.daily_target); habitActions.appendChild(menuButton); habitHeader.appendChild(habitActions); habitItem.appendChild(habitHeader); const dateGrid = document.createElement('div'); dateGrid.className = 'date-grid'; last30Days.forEach(date => { const dateSquare = document.createElement('div'); dateSquare.className = 'date-square'; const dateCount = habit.completed_dates[date] || 0; const isCompleted = dateCount >= dailyTarget; // Apply gradient styling based on completion percentage applyCompletionStyling(dateSquare, dateCount, dailyTarget, habitColor); // Show count in tooltip if target > 1 if (dailyTarget > 1) { dateSquare.title = `${date}: ${dateCount}/${dailyTarget}`; } else { dateSquare.title = date; } dateGrid.appendChild(dateSquare); }); habitItem.appendChild(dateGrid); // Streak Counter hinzufügen const streakContainer = document.createElement('div'); streakContainer.className = 'streak-container'; const flameIcon = document.createElement('span'); flameIcon.className = 'flame-icon'; flameIcon.innerHTML = '🔥'; // Flammen-Emoji const streakCount = document.createElement('span'); streakCount.className = 'streak-count'; const currentStreak = calculateStreak(habit.completed_dates, dailyTarget); streakCount.textContent = `${currentStreak} Tag${currentStreak !== 1 ? 'e' : ''}`; streakContainer.appendChild(flameIcon); streakContainer.appendChild(streakCount); habitItem.appendChild(streakContainer); habitListDiv.appendChild(habitItem); }); } async function addHabit() { const newHabitNameInput = document.getElementById('newHabitName'); const name = newHabitNameInput.value.trim(); if (name) { try { const response = await fetch('/habits', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name }) }); const newHabit = await response.json(); if (response.ok) { newHabitNameInput.value = ''; fetchHabits(); // Re-fetch and re-render all habits } else { console.error('Failed to add habit:', newHabit.error); } } catch (error) { console.error('Error adding habit:', error); } } } async function openHabitModal(habitId, habitName, completedDates, habitColor = '#4CAF50', dailyTarget = 1) { currentHabitId = habitId; currentHabitCompletedDates = completedDates; currentHabitColor = habitColor; currentHabitDailyTarget = dailyTarget; currentModalDate = new Date(); // Reset to current month modalHabitName.textContent = habitName; // Setzen der habitId auf den Buttons im Modal modalCompleteTodayBtn.dataset.habitId = habitId; modalEditBtn.dataset.habitId = habitId; modalDeleteBtn.dataset.habitId = habitId; // Set color picker to current habit color document.getElementById('habitColorPicker').value = habitColor; // Set daily target input to current value document.getElementById('habitDailyTarget').value = dailyTarget; // Set CSS variable for modal elements dateModal.style.setProperty('--current-habit-color', habitColor); // Set contrast color for text on colored backgrounds const contrastColor = getContrastColor(habitColor); dateModal.style.setProperty('--current-habit-text-color', contrastColor); renderModalCalendar(); dateModal.style.display = 'flex'; } function renderModalCalendar() { modalDateGrid.innerHTML = ''; // Update month/year display const monthNames = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']; document.getElementById('currentMonthYear').textContent = `${monthNames[currentModalDate.getMonth()]} ${currentModalDate.getFullYear()}`; const today = getCurrentDate(); const year = currentModalDate.getFullYear(); const month = currentModalDate.getMonth(); // Get first day of month and calculate starting position const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const startingDayOfWeek = (firstDay.getDay() + 6) % 7; // Monday = 0 // Add empty cells for days before month starts for (let i = 0; i < startingDayOfWeek; i++) { const emptyCell = document.createElement('div'); emptyCell.className = 'date-cell empty'; modalDateGrid.appendChild(emptyCell); } // Add days of the month for (let day = 1; day <= lastDay.getDate(); day++) { const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const dateCell = document.createElement('div'); dateCell.className = 'date-cell'; dateCell.textContent = day; const dateCount = currentHabitCompletedDates[dateStr] || 0; const isCompleted = dateCount >= currentHabitDailyTarget; if (isCompleted) { dateCell.classList.add('completed'); } // Use the new display function for consistent formatting updateDateCellDisplay(dateCell, dateStr, dateCount); if (dateStr === today) { dateCell.classList.add('today'); } dateCell.dataset.date = dateStr; dateCell.onclick = (event) => toggleCompletionForDate(event, currentHabitId, dateStr); modalDateGrid.appendChild(dateCell); } } function changeMonth(direction) { currentModalDate.setMonth(currentModalDate.getMonth() + direction); renderModalCalendar(); } function closeModal() { dateModal.style.display = 'none'; currentHabitId = null; // Zurücksetzen, wenn das Modal geschlossen wird fetchHabits(); // Refresh habits after closing modal } async function toggleCompletionForDate(event, habitId, date) { // Find the actual date cell, even if clicked on child elements let cell = event.target; if (!cell.classList.contains('date-cell')) { cell = cell.closest('.date-cell'); } if (!cell) return; // Safety check let response; let data; const currentCount = currentHabitCompletedDates[date] || 0; const shouldDecrement = event.shiftKey || event.ctrlKey || currentCount >= currentHabitDailyTarget; if (shouldDecrement && currentCount > 0) { // Decrement completion count response = await fetch(`/habits/${habitId}/uncomplete`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ date: date }) }); } else { // Increment completion count response = await fetch(`/habits/${habitId}/complete`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ date: date }) }); } data = await response.json(); if (response.ok) { // Update local completed dates object currentHabitCompletedDates[date] = data.current_count; // Update cell appearance const newCount = data.current_count; const isCompleted = newCount >= currentHabitDailyTarget; if (isCompleted) { cell.classList.add('completed'); } else { cell.classList.remove('completed'); } // Update cell text display updateDateCellDisplay(cell, date, newCount); } } async function completeHabitForDate(event, habitId, date) { // Der habitId kommt jetzt direkt vom dataset des Buttons oder der Funktion, die ihn aufruft // Sicherstellen, dass habitId gültig ist if (!habitId) { console.error('Habit ID is undefined for completion.'); return; } try { const response = await fetch(`/habits/${habitId}/complete`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ date: date }) }); const data = await response.json(); if (response.ok) { // Update the modal's date grid immediately const dateCells = modalDateGrid.querySelectorAll('.date-cell'); dateCells.forEach(cell => { if (cell.dataset.date === date) { cell.classList.add('completed'); } }); } else { console.error('Failed to complete habit:', data.error); } } catch (error) { console.error('Error completing habit:', error); } } async function editHabitName() { // Sicherstellen, dass currentHabitId gesetzt ist if (!currentHabitId) { console.error('No habit selected for editing.'); return; } const newName = prompt("Neuen Namen für die Gewohnheit eingeben:", modalHabitName.textContent); if (newName && newName.trim() !== "") { try { const response = await fetch(`/habits/${currentHabitId}`, { // Verwendet currentHabitId method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: newName.trim() }) }); const data = await response.json(); if (response.ok) { modalHabitName.textContent = newName.trim(); fetchHabits(); // Re-fetch and re-render all habits to update the main view } else { console.error('Failed to update habit:', data.error); } } catch (error) { console.error('Error updating habit:', error); } } } async function deleteHabitFromModal() { // Sicherstellen, dass currentHabitId gesetzt ist if (!currentHabitId) { console.error('No habit selected for deletion.'); return; } if (confirm("Bist du sicher, dass du diese Gewohnheit löschen möchtest?")) { try { const response = await fetch(`/habits/${currentHabitId}`, { // Verwendet currentHabitId method: 'DELETE' }); const data = await response.json(); if (response.ok) { closeModal(); fetchHabits(); // Re-fetch and re-render all habits } else { console.error('Failed to delete habit:', data.error); } } catch (error) { console.error('Error deleting habit:', error); } } } async function toggleTodayCompletion(habitId, date, buttonElement, dailyTarget = 1) { try { // Always increment completion count const response = await fetch(`/habits/${habitId}/complete`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ date: date }) }); const data = await response.json(); if (response.ok) { const newCount = data.current_count; const isCompleted = newCount >= dailyTarget; // Update button appearance and text if (dailyTarget > 1) { buttonElement.innerHTML = `${newCount}/${dailyTarget}`; } buttonElement.className = isCompleted ? 'completed-today' : 'not-completed-today'; // Refresh the date grid to show updated completion status fetchHabits(); } else { console.error('Failed to toggle habit completion:', data.error); } } catch (error) { console.error('Error toggling habit completion:', error); } } async function updateHabitColor() { if (!currentHabitId) { console.error('No habit selected for color update.'); return; } const newColor = document.getElementById('habitColorPicker').value; currentHabitColor = newColor; try { const response = await fetch(`/habits/${currentHabitId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ color: newColor }) }); const data = await response.json(); if (response.ok) { // Update CSS variable for modal elements dateModal.style.setProperty('--current-habit-color', newColor); // Update contrast color for text on colored backgrounds const contrastColor = getContrastColor(newColor); dateModal.style.setProperty('--current-habit-text-color', contrastColor); // Update modal calendar colors immediately updateModalColors(newColor); // Refresh habits list to show new color fetchHabits(); } else { console.error('Failed to update habit color:', data.error); } } catch (error) { console.error('Error updating habit color:', error); } } function updateModalColors(color) { // Update all date cells in modal to use new color const allCells = modalDateGrid.querySelectorAll('.date-cell'); allCells.forEach(cell => { const dateStr = cell.dataset.date; if (dateStr) { const dateCount = currentHabitCompletedDates[dateStr] || 0; // Re-apply styling with new color applyModalCompletionStyling(cell, dateCount, currentHabitDailyTarget, color); } }); } function getContrastColor(hexColor) { // Convert hex to RGB const r = parseInt(hexColor.slice(1, 3), 16); const g = parseInt(hexColor.slice(3, 5), 16); const b = parseInt(hexColor.slice(5, 7), 16); // Calculate luminance const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; // Return dark text for light colors, light text for dark colors return luminance > 0.5 ? '#333333' : '#ffffff'; } async function updateDailyTarget() { if (!currentHabitId) { console.error('No habit selected for daily target update.'); return; } const newTarget = parseInt(document.getElementById('habitDailyTarget').value); if (newTarget < 1 || newTarget > 20) { alert('Tägliches Ziel muss zwischen 1 und 20 liegen.'); return; } currentHabitDailyTarget = newTarget; try { const response = await fetch(`/habits/${currentHabitId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ daily_target: newTarget }) }); const data = await response.json(); if (response.ok) { // Re-render modal calendar with new target renderModalCalendar(); // Refresh habits list to show new target fetchHabits(); } else { console.error('Failed to update daily target:', data.error); } } catch (error) { console.error('Error updating daily target:', error); } } function updateDateCellDisplay(cell, date, count) { const day = date.split('-')[2]; // Get day from YYYY-MM-DD format // Clear any existing content first cell.innerHTML = ''; // Apply gradient styling for modal cells applyModalCompletionStyling(cell, count, currentHabitDailyTarget, currentHabitColor); if (currentHabitDailyTarget > 1) { // Create structured display with date on top, counter below const dayDiv = document.createElement('div'); dayDiv.className = 'date-day'; dayDiv.textContent = parseInt(day); const counterDiv = document.createElement('div'); counterDiv.className = 'date-counter'; counterDiv.textContent = `${count}/${currentHabitDailyTarget}`; cell.appendChild(dayDiv); cell.appendChild(counterDiv); } else { cell.textContent = parseInt(day); } } function applyCompletionStyling(element, currentCount, targetCount, color) { // Remove existing classes element.classList.remove('completed', 'partial'); if (currentCount === 0) { // No completion - default background return; } else if (currentCount >= targetCount) { // Fully completed - solid color element.classList.add('completed'); } else { // Partially completed - gradient element.classList.add('partial'); const percentage = Math.round((currentCount / targetCount) * 100); element.style.setProperty('--completion-percentage', `${percentage}%`); element.style.setProperty('--habit-color', color); } } function applyModalCompletionStyling(element, currentCount, targetCount, color) { // Remove existing classes element.classList.remove('completed', 'partial'); if (currentCount === 0) { // No completion - default background and text element.style.removeProperty('color'); return; } else if (currentCount >= targetCount) { // Fully completed - solid color with contrast text element.classList.add('completed'); const contrastColor = getContrastColor(color); element.style.setProperty('color', contrastColor); } else { // Partially completed - gradient with smart text color element.classList.add('partial'); const percentage = Math.round((currentCount / targetCount) * 100); element.style.setProperty('--completion-percentage', `${percentage}%`); element.style.setProperty('--current-habit-color', color); // For partial completion, use contrast color if more than 50% completed if (percentage > 50) { const contrastColor = getContrastColor(color); element.style.setProperty('color', contrastColor); } else { // Use default text color for low completion element.style.setProperty('color', '#e0e0e0'); } } }