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: