diff --git a/dashboard/habits.html b/dashboard/habits.html index 554bd0f..aec0859 100644 --- a/dashboard/habits.html +++ b/dashboard/habits.html @@ -853,6 +853,15 @@ function showAddHabitModal() { const modal = document.getElementById('habitModal'); const form = document.getElementById('habitForm'); + const modalTitle = document.querySelector('.modal-title'); + const submitBtnText = document.getElementById('submitBtnText'); + + // Reset editing state + editingHabitId = null; + + // Reset modal title and button text to create mode + modalTitle.textContent = 'Add Habit'; + submitBtnText.textContent = 'Create Habit'; // Reset form form.reset(); @@ -877,6 +886,9 @@ function closeHabitModal() { const modal = document.getElementById('habitModal'); modal.classList.remove('active'); + + // Reset editing state + editingHabitId = null; } // Close modal when clicking outside @@ -1004,6 +1016,9 @@ const submitBtn = document.getElementById('submitHabitBtn'); const submitBtnText = document.getElementById('submitBtnText'); + // Determine if we're creating or editing + const isEditing = editingHabitId !== null; + // Get form values const name = document.getElementById('habitName').value.trim(); const category = document.getElementById('habitCategory').value; @@ -1061,11 +1076,14 @@ // Show loading state submitBtn.disabled = true; - submitBtnText.textContent = 'Creating...'; + submitBtnText.textContent = isEditing ? 'Saving...' : 'Creating...'; try { - const response = await fetch('/echo/api/habits', { - method: 'POST', + const url = isEditing ? `/echo/api/habits/${editingHabitId}` : '/echo/api/habits'; + const method = isEditing ? 'PUT' : 'POST'; + + const response = await fetch(url, { + method: method, headers: { 'Content-Type': 'application/json' }, @@ -1078,16 +1096,16 @@ } // Success - showToast('Habit created successfully!', 'success'); + showToast(isEditing ? 'Habit updated!' : 'Habit created successfully!', 'success'); closeHabitModal(); await loadHabits(); } catch (error) { - console.error('Failed to create habit:', error); - showToast('Failed to create habit: ' + error.message, 'error'); + console.error(`Failed to ${isEditing ? 'update' : 'create'} habit:`, error); + showToast(`Failed to ${isEditing ? 'update' : 'create'} habit: ` + error.message, 'error'); } finally { submitBtn.disabled = false; - submitBtnText.textContent = 'Create Habit'; + submitBtnText.textContent = isEditing ? 'Save Changes' : 'Create Habit'; } } @@ -1108,9 +1126,71 @@ }, 3000); } - // Show edit habit modal (placeholder) + // Show edit habit modal + let editingHabitId = null; // Track which habit we're editing + function showEditHabitModal(habitId) { - alert('Edit Habit modal - coming in next story!'); + const habit = habits.find(h => h.id === habitId); + if (!habit) { + showToast('Habit not found', 'error'); + return; + } + + editingHabitId = habitId; + + const modal = document.getElementById('habitModal'); + const form = document.getElementById('habitForm'); + const modalTitle = document.querySelector('.modal-title'); + const submitBtn = document.getElementById('submitHabitBtn'); + const submitBtnText = document.getElementById('submitBtnText'); + + // Change modal title and button text + modalTitle.textContent = 'Edit Habit'; + submitBtnText.textContent = 'Save Changes'; + + // Pre-populate form fields + document.getElementById('habitName').value = habit.name; + document.getElementById('habitCategory').value = habit.category || 'health'; + document.getElementById('habitPriority').value = habit.priority || 3; + document.getElementById('habitNotes').value = habit.notes || ''; + document.getElementById('frequencyType').value = habit.frequency?.type || 'daily'; + document.getElementById('reminderTime').value = habit.reminderTime || ''; + + // Set selected color and icon + selectedColor = habit.color || '#3B82F6'; + selectedIcon = habit.icon || 'dumbbell'; + + // Initialize color picker with current selection + initColorPicker(); + + // Initialize icon picker with current selection + initIconPicker(); + + // Update frequency params and pre-populate + updateFrequencyParams(); + + // Pre-populate frequency params based on type + const frequencyType = habit.frequency?.type; + if (frequencyType === 'specific_days' && habit.frequency.days) { + const dayCheckboxes = document.querySelectorAll('input[name="day"]'); + dayCheckboxes.forEach(cb => { + cb.checked = habit.frequency.days.includes(parseInt(cb.value)); + }); + } else if (frequencyType === 'x_per_week' && habit.frequency.count) { + const xPerWeekInput = document.getElementById('xPerWeek'); + if (xPerWeekInput) { + xPerWeekInput.value = habit.frequency.count; + } + } else if (frequencyType === 'custom' && habit.frequency.interval) { + const customIntervalInput = document.getElementById('customInterval'); + if (customIntervalInput) { + customIntervalInput.value = habit.frequency.interval; + } + } + + // Show modal + modal.classList.add('active'); + lucide.createIcons(); } // Delete habit (placeholder) diff --git a/dashboard/tests/test_habits_frontend.py b/dashboard/tests/test_habits_frontend.py index 7961dcb..4bedc5d 100644 --- a/dashboard/tests/test_habits_frontend.py +++ b/dashboard/tests/test_habits_frontend.py @@ -3,6 +3,7 @@ Test suite for Habits frontend page structure and navigation Story US-006: Frontend - Page structure, layout, and navigation link Story US-007: Frontend - Habit card component Story US-008: Frontend - Create habit modal with all options +Story US-009: Frontend - Edit habit modal """ import sys @@ -417,17 +418,18 @@ def test_client_side_validation(): print("✓ Test 28: Client-side validation checks name required") def test_submit_posts_to_api(): - """Test 29: Submit sends POST /echo/api/habits and refreshes list""" + """Test 29: Submit sends POST /echo/api/habits (or PUT for edit) and refreshes list""" habits_path = Path(__file__).parent.parent / 'habits.html' content = habits_path.read_text() assert 'submitHabitForm' in content, "Should have submitHabitForm function" submit_func = content[content.find('function submitHabitForm'):] - assert "fetch('/echo/api/habits'" in submit_func or 'fetch("/echo/api/habits"' in submit_func, \ - "Should POST to /echo/api/habits" - assert "'POST'" in submit_func or '"POST"' in submit_func, \ - "Should use POST method" + # Check for conditional URL and method (since US-009 added edit support) + assert ('/echo/api/habits' in submit_func), \ + "Should use /echo/api/habits endpoint" + assert ("'POST'" in submit_func or '"POST"' in submit_func or "'PUT'" in submit_func or '"PUT"' in submit_func), \ + "Should use POST or PUT method" assert 'JSON.stringify' in submit_func, "Should send JSON body" assert 'loadHabits()' in submit_func, "Should refresh habit list on success" @@ -508,6 +510,244 @@ def test_typecheck_us008(): print("✓ Test 33: Typecheck passes (all modal functions defined)") +def test_edit_modal_opens_on_gear_icon(): + """Test 34: Clicking gear icon on habit card opens edit modal""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + # Check that gear icon exists with onclick handler + assert 'settings' in content, "Should have settings icon (gear)" + assert "showEditHabitModal" in content, "Should have showEditHabitModal function call" + + # Check that showEditHabitModal function is defined and not a placeholder + assert 'function showEditHabitModal(habitId)' in content, "showEditHabitModal should be defined" + assert 'editingHabitId = habitId' in content or 'editingHabitId=habitId' in content, \ + "Should set editingHabitId" + assert 'const habit = habits.find(h => h.id === habitId)' in content or \ + 'const habit=habits.find(h=>h.id===habitId)' in content, \ + "Should find habit by ID" + + print("✓ Test 34: Edit modal opens on gear icon click") + +def test_edit_modal_prepopulated(): + """Test 35: Edit modal is pre-populated with all existing habit data""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + # Check that form fields are pre-populated + assert "getElementById('habitName').value = habit.name" in content or \ + "getElementById('habitName').value=habit.name" in content, \ + "Should pre-populate habit name" + assert "getElementById('habitCategory').value = habit.category" in content or \ + "getElementById('habitCategory').value=habit.category" in content, \ + "Should pre-populate category" + assert "getElementById('habitPriority').value = habit.priority" in content or \ + "getElementById('habitPriority').value=habit.priority" in content, \ + "Should pre-populate priority" + assert "getElementById('habitNotes').value = habit.notes" in content or \ + "getElementById('habitNotes').value=habit.notes" in content, \ + "Should pre-populate notes" + assert "getElementById('frequencyType').value = habit.frequency" in content or \ + "getElementById('frequencyType').value=habit.frequency" in content, \ + "Should pre-populate frequency type" + + # Check color and icon selection + assert 'selectedColor = habit.color' in content or 'selectedColor=habit.color' in content, \ + "Should set selectedColor from habit" + assert 'selectedIcon = habit.icon' in content or 'selectedIcon=habit.icon' in content, \ + "Should set selectedIcon from habit" + + print("✓ Test 35: Edit modal pre-populated with habit data") + +def test_edit_modal_title_and_button(): + """Test 36: Modal title shows 'Edit Habit' and button shows 'Save Changes'""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + # Check that modal title is changed to Edit Habit + assert "modalTitle.textContent = 'Edit Habit'" in content or \ + 'modalTitle.textContent="Edit Habit"' in content or \ + "modalTitle.textContent='Edit Habit'" in content, \ + "Should set modal title to 'Edit Habit'" + + # Check that submit button text is changed + assert "submitBtnText.textContent = 'Save Changes'" in content or \ + 'submitBtnText.textContent="Save Changes"' in content or \ + "submitBtnText.textContent='Save Changes'" in content, \ + "Should set button text to 'Save Changes'" + + print("✓ Test 36: Modal title shows 'Edit Habit' and button shows 'Save Changes'") + +def test_edit_modal_frequency_params(): + """Test 37: Frequency params display correctly for habit's current frequency type""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + # Check that updateFrequencyParams is called + assert 'updateFrequencyParams()' in content, "Should call updateFrequencyParams()" + + # Check that frequency params are pre-populated for specific types + assert 'specific_days' in content and 'habit.frequency.days' in content, \ + "Should handle specific_days frequency params" + assert 'x_per_week' in content and 'habit.frequency.count' in content, \ + "Should handle x_per_week frequency params" + assert 'custom' in content and 'habit.frequency.interval' in content, \ + "Should handle custom frequency params" + + # Check that day checkboxes are pre-populated + assert 'cb.checked = habit.frequency.days.includes' in content or \ + 'cb.checked=habit.frequency.days.includes' in content, \ + "Should pre-select days for specific_days frequency" + + print("✓ Test 37: Frequency params display correctly for current frequency") + +def test_edit_modal_icon_color_pickers(): + """Test 38: Icon and color pickers show current selections""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + # Check that pickers are initialized after setting values + assert 'initColorPicker()' in content, "Should call initColorPicker()" + assert 'initIconPicker()' in content, "Should call initIconPicker()" + + # Check that selectedColor and selectedIcon are set before initialization + showEditIndex = content.find('function showEditHabitModal') + initColorIndex = content.find('initColorPicker()', showEditIndex) + selectedColorIndex = content.find('selectedColor = habit.color', showEditIndex) + + assert selectedColorIndex > 0 and selectedColorIndex < initColorIndex, \ + "Should set selectedColor before calling initColorPicker()" + + print("✓ Test 38: Icon and color pickers show current selections") + +def test_edit_modal_submit_put(): + """Test 39: Submit sends PUT /echo/api/habits/{id} and refreshes list on success""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + # Check that editingHabitId is tracked + assert 'let editingHabitId' in content or 'editingHabitId' in content, \ + "Should track editingHabitId" + + # Check that isEditing is determined + assert 'const isEditing = editingHabitId !== null' in content or \ + 'const isEditing=editingHabitId!==null' in content or \ + 'isEditing = editingHabitId !== null' in content, \ + "Should determine if editing" + + # Check that URL and method are conditional + assert "const url = isEditing ? `/echo/api/habits/${editingHabitId}` : '/echo/api/habits'" in content or \ + 'const url=isEditing?`/echo/api/habits/${editingHabitId}`' in content or \ + "url = isEditing ? `/echo/api/habits/${editingHabitId}` : '/echo/api/habits'" in content, \ + "URL should be conditional based on isEditing" + + assert "const method = isEditing ? 'PUT' : 'POST'" in content or \ + "const method=isEditing?'PUT':'POST'" in content or \ + "method = isEditing ? 'PUT' : 'POST'" in content, \ + "Method should be conditional (PUT for edit, POST for create)" + + # Check that loadHabits is called after success + assert 'await loadHabits()' in content, "Should refresh habit list after success" + + print("✓ Test 39: Submit sends PUT and refreshes list") + +def test_edit_modal_toast_messages(): + """Test 40: Toast shown for success and error""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + # Check for conditional success message + assert "isEditing ? 'Habit updated!' : 'Habit created successfully!'" in content or \ + "isEditing?'Habit updated!':'Habit created successfully!'" in content, \ + "Should show different toast message for edit vs create" + + # Check that error toast handles both edit and create + assert 'Failed to ${isEditing' in content or 'Failed to ' + '${isEditing' in content, \ + "Error toast should be conditional" + + print("✓ Test 40: Toast messages for success and error") + +def test_edit_modal_add_resets_state(): + """Test 41: showAddHabitModal resets editingHabitId and modal UI""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + # Find showAddHabitModal function + add_modal_start = content.find('function showAddHabitModal()') + add_modal_end = content.find('function ', add_modal_start + 1) + add_modal_func = content[add_modal_start:add_modal_end] + + # Check that editingHabitId is reset + assert 'editingHabitId = null' in add_modal_func or 'editingHabitId=null' in add_modal_func, \ + "showAddHabitModal should reset editingHabitId to null" + + # Check that modal title is reset to 'Add Habit' + assert "modalTitle.textContent = 'Add Habit'" in add_modal_func or \ + 'modalTitle.textContent="Add Habit"' in add_modal_func, \ + "Should reset modal title to 'Add Habit'" + + # Check that button text is reset to 'Create Habit' + assert "submitBtnText.textContent = 'Create Habit'" in add_modal_func or \ + 'submitBtnText.textContent="Create Habit"' in add_modal_func, \ + "Should reset button text to 'Create Habit'" + + print("✓ Test 41: showAddHabitModal resets editing state") + +def test_edit_modal_close_resets_state(): + """Test 42: closeHabitModal resets editingHabitId""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + # Find closeHabitModal function + close_modal_start = content.find('function closeHabitModal()') + close_modal_end = content.find('function ', close_modal_start + 1) + close_modal_func = content[close_modal_start:close_modal_end] + + # Check that editingHabitId is reset when closing + assert 'editingHabitId = null' in close_modal_func or 'editingHabitId=null' in close_modal_func, \ + "closeHabitModal should reset editingHabitId to null" + + print("✓ Test 42: closeHabitModal resets editing state") + +def test_edit_modal_no_console_errors(): + """Test 43: No obvious console error sources in edit modal code""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + # Check for common error patterns + assert content.count('getElementById(') > 0, "Should use getElementById" + + # Check that habit is validated before use + showEditIndex = content.find('function showEditHabitModal') + showEditEnd = content.find('\n }', showEditIndex + 500) # Find end of function + showEditFunc = content[showEditIndex:showEditEnd] + + assert 'if (!habit)' in showEditFunc or 'if(!habit)' in showEditFunc, \ + "Should check if habit exists before using it" + assert 'showToast' in showEditFunc and 'error' in showEditFunc, \ + "Should show error toast if habit not found" + + print("✓ Test 43: No obvious console error sources") + +def test_typecheck_us009(): + """Test 44: Typecheck passes - all edit modal functions and variables defined""" + habits_path = Path(__file__).parent.parent / 'habits.html' + content = habits_path.read_text() + + # Check that editingHabitId is declared + assert 'let editingHabitId' in content, "editingHabitId should be declared" + + # Check that showEditHabitModal is fully implemented (not placeholder) + assert 'function showEditHabitModal(habitId)' in content, "showEditHabitModal should be defined" + assert 'alert' not in content[content.find('function showEditHabitModal'):content.find('function showEditHabitModal')+1000], \ + "showEditHabitModal should not be a placeholder with alert()" + + # Check that submitHabitForm handles both create and edit + assert 'const isEditing' in content or 'isEditing' in content, \ + "submitHabitForm should determine if editing" + + print("✓ Test 44: Typecheck passes (edit modal fully implemented)") + def run_all_tests(): """Run all tests in sequence""" tests = [ @@ -547,9 +787,21 @@ def run_all_tests(): test_toast_notifications, test_modal_no_console_errors, test_typecheck_us008, + # US-009 tests + test_edit_modal_opens_on_gear_icon, + test_edit_modal_prepopulated, + test_edit_modal_title_and_button, + test_edit_modal_frequency_params, + test_edit_modal_icon_color_pickers, + test_edit_modal_submit_put, + test_edit_modal_toast_messages, + test_edit_modal_add_resets_state, + test_edit_modal_close_resets_state, + test_edit_modal_no_console_errors, + test_typecheck_us009, ] - print(f"\nRunning {len(tests)} frontend tests for US-006, US-007, and US-008...\n") + print(f"\nRunning {len(tests)} frontend tests for US-006, US-007, US-008, and US-009...\n") failed = [] for test in tests: