diff --git a/app.py b/app.py index 8b9449e..2c9d71b 100644 --- a/app.py +++ b/app.py @@ -29,16 +29,34 @@ def get_habits(): last_30_days = get_last_30_days() for habit_doc in raw_habits: + # Handle migration from old format (list) to new format (dict) + completed_dates = habit_doc.get('completed_dates', {}) + if isinstance(completed_dates, list): + # Convert old format to new format + new_completed_dates = {} + for date in completed_dates: + new_completed_dates[date] = 1 + completed_dates = new_completed_dates + # Update in database + db.update({'completed_dates': completed_dates}, doc_ids=[habit_doc.doc_id]) + current_habit_data = { 'id': habit_doc.doc_id, 'name': habit_doc.get('name'), - 'completed_dates': habit_doc.get('completed_dates', []), - 'color': habit_doc.get('color', '#4CAF50') + 'completed_dates': completed_dates, + 'color': habit_doc.get('color', '#4CAF50'), + 'daily_target': habit_doc.get('daily_target', 1) } completion_history = {} for date_str in last_30_days: - completion_history[date_str] = date_str in current_habit_data['completed_dates'] + completed_count = completed_dates.get(date_str, 0) + target_count = current_habit_data['daily_target'] + completion_history[date_str] = { + 'completed': completed_count, + 'target': target_count, + 'is_complete': completed_count >= target_count + } current_habit_data['completion_history'] = completion_history formatted_habits.append(current_habit_data) @@ -50,11 +68,12 @@ def add_habit(): data = request.json name = data.get('name') color = data.get('color', '#4CAF50') # Default green color + daily_target = data.get('daily_target', 1) # Default 1x per day if not name: return jsonify({'error': 'Habit name is required'}), 400 - habit_id = db.insert({'name': name, 'completed_dates': [], 'color': color}) - return jsonify({'id': habit_id, 'name': name, 'completed_dates': [], 'color': color}), 201 + habit_id = db.insert({'name': name, 'completed_dates': {}, 'color': color, 'daily_target': daily_target}) + return jsonify({'id': habit_id, 'name': name, 'completed_dates': {}, 'color': color, 'daily_target': daily_target}), 201 @app.route('/habits//complete', methods=['POST']) def complete_habit(habit_id): @@ -64,11 +83,25 @@ def complete_habit(habit_id): if not habit: return jsonify({'error': 'Habit not found'}), 404 - if date_to_complete not in habit['completed_dates']: - habit['completed_dates'].append(date_to_complete) - db.update({'completed_dates': habit['completed_dates']}, doc_ids=[habit_id]) + # Handle migration from old format (list) to new format (dict) + completed_dates = habit.get('completed_dates', {}) + if isinstance(completed_dates, list): + new_completed_dates = {} + for date in completed_dates: + new_completed_dates[date] = 1 + completed_dates = new_completed_dates - return jsonify({'message': f'Habit {habit_id} marked as completed for {date_to_complete}', 'completed_dates': habit['completed_dates']}) + # Increment completion count for the date + current_count = completed_dates.get(date_to_complete, 0) + completed_dates[date_to_complete] = current_count + 1 + + db.update({'completed_dates': completed_dates}, doc_ids=[habit_id]) + + return jsonify({ + 'message': f'Habit {habit_id} marked as completed for {date_to_complete}', + 'completed_dates': completed_dates, + 'current_count': completed_dates[date_to_complete] + }) @app.route('/habits//uncomplete', methods=['POST']) def uncomplete_habit(habit_id): @@ -78,20 +111,39 @@ def uncomplete_habit(habit_id): if not habit: return jsonify({'error': 'Habit not found'}), 404 - if date_to_uncomplete in habit['completed_dates']: - habit['completed_dates'].remove(date_to_uncomplete) - db.update({'completed_dates': habit['completed_dates']}, doc_ids=[habit_id]) + # Handle migration from old format (list) to new format (dict) + completed_dates = habit.get('completed_dates', {}) + if isinstance(completed_dates, list): + new_completed_dates = {} + for date in completed_dates: + new_completed_dates[date] = 1 + completed_dates = new_completed_dates - return jsonify({'message': f'Habit {habit_id} marked as uncompleted for {date_to_uncomplete}', 'completed_dates': habit['completed_dates']}) + # Decrement completion count for the date + if date_to_uncomplete in completed_dates: + current_count = completed_dates[date_to_uncomplete] + if current_count > 1: + completed_dates[date_to_uncomplete] = current_count - 1 + else: + del completed_dates[date_to_uncomplete] + + db.update({'completed_dates': completed_dates}, doc_ids=[habit_id]) + + return jsonify({ + 'message': f'Habit {habit_id} marked as uncompleted for {date_to_uncomplete}', + 'completed_dates': completed_dates, + 'current_count': completed_dates.get(date_to_uncomplete, 0) + }) @app.route('/habits/', methods=['PUT']) def update_habit(habit_id): data = request.json name = data.get('name') color = data.get('color') + daily_target = data.get('daily_target') - if not name and not color: - return jsonify({'error': 'Habit name or color is required'}), 400 + if not name and not color and not daily_target: + return jsonify({'error': 'Habit name, color, or daily target is required'}), 400 habit = db.get(doc_id=habit_id) if not habit: @@ -102,6 +154,8 @@ def update_habit(habit_id): update_data['name'] = name if color: update_data['color'] = color + if daily_target: + update_data['daily_target'] = int(daily_target) db.update(update_data, doc_ids=[habit_id]) return jsonify({'message': f'Habit {habit_id} updated', **update_data}) diff --git a/habits.json b/habits.json index e8adf5c..ccba443 100644 --- a/habits.json +++ b/habits.json @@ -1 +1 @@ -{"_default": {"1": {"name": "Mega Zock", "completed_dates": ["2025-07-14", "2025-08-01", "2025-08-09", "2025-08-17", "2025-07-01", "2025-07-16", "2025-07-10", "2025-07-15", "2025-07-09", "2025-07-11", "2025-07-12", "2025-07-13", "2025-07-06", "2025-07-05", "2025-07-04", "2025-07-03", "2025-07-02", "2025-07-08", "2025-07-07"], "color": "#08680c"}, "2": {"name": "Laufen", "completed_dates": [], "color": "#1a3ed1"}}} \ No newline at end of file +{"_default": {"1": {"name": "Mega Zock", "completed_dates": {"2025-07-14": 1, "2025-08-01": 1, "2025-08-09": 1, "2025-08-17": 1, "2025-07-01": 1, "2025-07-16": 1, "2025-07-10": 1, "2025-07-15": 1, "2025-07-09": 1, "2025-07-11": 1, "2025-07-12": 1, "2025-07-13": 1, "2025-07-06": 1, "2025-07-05": 1, "2025-07-04": 1, "2025-07-03": 1, "2025-07-02": 1, "2025-07-08": 1, "2025-07-07": 1}, "color": "#14cc1a"}, "2": {"name": "Laufen", "completed_dates": {}, "color": "#1a3ed1"}, "3": {"name": "Essen", "completed_dates": {"2025-07-18": 20, "2025-07-19": 5, "2025-07-17": 14, "2025-07-10": 1, "2025-07-11": 1, "2025-07-12": 1, "2025-07-20": 6, "2025-07-16": 1, "2025-07-15": 11, "2025-07-26": 3}, "color": "#fcfcfc", "daily_target": 20}}} \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css index 5a3062b..2206713 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -137,6 +137,10 @@ h1 { background-color: var(--habit-color, #4CAF50); } +.date-square.partial { + background: linear-gradient(to right, var(--habit-color, #4CAF50) var(--completion-percentage, 50%), #555 var(--completion-percentage, 50%)); +} + /* Modal Styles */ .modal { display: none; @@ -258,6 +262,11 @@ h1 { color: var(--current-habit-text-color, white); } +.date-cell.partial { + background: linear-gradient(to right, var(--current-habit-color, #4CAF50) var(--completion-percentage, 50%), #444 var(--completion-percentage, 50%)); + color: #e0e0e0; +} + .date-cell.today { border: 2px solid #007bff; /* Highlight today */ @@ -268,6 +277,19 @@ h1 { cursor: default; } +.date-day { + font-size: 1em; + font-weight: bold; + line-height: 1; +} + +.date-counter { + font-size: 0.7em; + opacity: 0.8; + line-height: 1; + margin-top: 2px; +} + .streak-container { display: flex; align-items: center; @@ -356,4 +378,31 @@ h1 { border-radius: 50%; cursor: pointer; background: none; +} + +.daily-target-section { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 20px; + gap: 10px; +} + +.daily-target-section label { + color: #e0e0e0; + font-weight: bold; +} + +#habitDailyTarget { + width: 60px; + padding: 5px; + border: 1px solid #444; + border-radius: 5px; + background-color: #333; + color: #e0e0e0; + text-align: center; +} + +.daily-target-section span { + color: #e0e0e0; } \ No newline at end of file diff --git a/static/js/script.js b/static/js/script.js index 46442bf..eb2fb5d 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -11,8 +11,9 @@ const modalDeleteBtn = document.getElementById('modalDeleteBtn'); // Neu hinzuge 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 +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(); @@ -35,28 +36,27 @@ function getPastDates(days) { return dates; } -function calculateStreak(completedDates) { - if (!completedDates || completedDates.length === 0) { +function calculateStreak(completedDates, dailyTarget) { + if (!completedDates || Object.keys(completedDates).length === 0) { return 0; } - // Sort dates in descending order (newest first) - const sortedDates = completedDates.sort((a, b) => new Date(b) - new Date(a)); const today = getCurrentDate(); - let streak = 0; let currentDate = new Date(); // Check if today is completed, if not, start from yesterday - if (!sortedDates.includes(today)) { + 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 (sortedDates.includes(dateStr)) { + if (count >= dailyTarget) { streak++; currentDate.setDate(currentDate.getDate() - 1); } else { @@ -101,19 +101,28 @@ function renderHabits(habits) { const habitActions = document.createElement('div'); habitActions.className = 'habit-actions'; - // Haken-Button für heute abhaken/rückgängig machen + // Haken-Button für heute abhaken/rückgängig machen mit Zähler const todayToggleButton = document.createElement('button'); const today = getCurrentDate(); - const isCompletedToday = habit.completed_dates && habit.completed_dates.includes(today); - todayToggleButton.innerHTML = isCompletedToday ? '✓' : '✓'; // Checkmark symbol + 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); + 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); + menuButton.onclick = () => openHabitModal(habit.id, habit.name, habit.completed_dates, habitColor, habit.daily_target); habitActions.appendChild(menuButton); habitHeader.appendChild(habitActions); habitItem.appendChild(habitHeader); @@ -124,10 +133,18 @@ function renderHabits(habits) { last30Days.forEach(date => { const dateSquare = document.createElement('div'); dateSquare.className = 'date-square'; - if (habit.completed_dates && habit.completed_dates.includes(date)) { - dateSquare.classList.add('completed'); + 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; } - dateSquare.title = date; // Show date on hover dateGrid.appendChild(dateSquare); }); habitItem.appendChild(dateGrid); @@ -142,7 +159,7 @@ function renderHabits(habits) { const streakCount = document.createElement('span'); streakCount.className = 'streak-count'; - const currentStreak = calculateStreak(habit.completed_dates); + const currentStreak = calculateStreak(habit.completed_dates, dailyTarget); streakCount.textContent = `${currentStreak} Tag${currentStreak !== 1 ? 'e' : ''}`; streakContainer.appendChild(flameIcon); @@ -177,10 +194,11 @@ async function addHabit() { } } -async function openHabitModal(habitId, habitName, completedDates, habitColor = '#4CAF50') { +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; @@ -192,6 +210,9 @@ async function openHabitModal(habitId, habitName, completedDates, habitColor = ' // 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); @@ -236,9 +257,15 @@ function renderModalCalendar() { dateCell.className = 'date-cell'; dateCell.textContent = day; - if (currentHabitCompletedDates.includes(dateStr)) { + 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'); } @@ -261,41 +288,54 @@ function closeModal() { } async function toggleCompletionForDate(event, habitId, date) { - const cell = event.target; + // 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; - if (cell.classList.contains('completed')) { - // Mark as uncompleted + 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 }) }); - data = await response.json(); - if (response.ok) { - cell.classList.remove('completed'); - // Update local completed dates array - const index = currentHabitCompletedDates.indexOf(date); - if (index > -1) { - currentHabitCompletedDates.splice(index, 1); - } - } } else { - // Mark as completed + // 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) { + } + + 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'); - // Update local completed dates array - if (!currentHabitCompletedDates.includes(date)) { - currentHabitCompletedDates.push(date); - } + } else { + cell.classList.remove('completed'); } + + // Update cell text display + updateDateCellDisplay(cell, date, newCount); } } @@ -381,37 +421,27 @@ async function deleteHabitFromModal() { } } -async function toggleTodayCompletion(habitId, date, buttonElement) { +async function toggleTodayCompletion(habitId, date, buttonElement, dailyTarget = 1) { try { - const isCurrentlyCompleted = buttonElement.classList.contains('completed-today'); - let response; - - if (isCurrentlyCompleted) { - // Mark as uncompleted - response = await fetch(`/habits/${habitId}/uncomplete`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ date: date }) - }); - } else { - // Mark as completed - response = await fetch(`/habits/${habitId}/complete`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ date: date }) - }); - } + // 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) { - // Update button appearance - if (isCurrentlyCompleted) { - buttonElement.classList.remove('completed-today'); - buttonElement.classList.add('not-completed-today'); - } else { - buttonElement.classList.remove('not-completed-today'); - buttonElement.classList.add('completed-today'); + 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 { @@ -459,10 +489,15 @@ async function updateHabitColor() { } function updateModalColors(color) { - // Update completed date cells in modal - const completedCells = modalDateGrid.querySelectorAll('.date-cell.completed'); - completedCells.forEach(cell => { - cell.style.backgroundColor = 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); + } }); } @@ -477,4 +512,114 @@ function getContrastColor(hexColor) { // 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'); + } + } } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index f8c58c5..755142b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -40,6 +40,11 @@ +
+ + + x pro Tag +