// Recurring expenses page JavaScript let currentRecurring = []; let detectedSuggestions = []; // Load user profile to get currency async function loadUserCurrency() { try { const profile = await apiCall('/api/settings/profile'); window.userCurrency = profile.profile.currency || 'GBP'; } catch (error) { console.error('Failed to load user currency:', error); window.userCurrency = 'GBP'; } } // Load recurring expenses async function loadRecurringExpenses() { try { const data = await apiCall('/api/recurring/'); currentRecurring = data.recurring_expenses || []; displayRecurringExpenses(currentRecurring); } catch (error) { console.error('Failed to load recurring expenses:', error); showToast(window.getTranslation('recurring.errorLoading', 'Failed to load recurring expenses'), 'error'); } } // Display recurring expenses function displayRecurringExpenses(recurring) { const container = document.getElementById('recurring-list'); if (!recurring || recurring.length === 0) { const noRecurringText = window.getTranslation('recurring.noRecurring', 'No recurring expenses yet'); const addFirstText = window.getTranslation('recurring.addFirst', 'Add your first recurring expense or detect patterns from existing expenses'); container.innerHTML = `
repeat

${noRecurringText}

${addFirstText}

`; return; } // Group by active status const active = recurring.filter(r => r.is_active); const inactive = recurring.filter(r => !r.is_active); let html = ''; if (active.length > 0) { html += '

' + window.getTranslation('recurring.active', 'Active Recurring Expenses') + '

'; html += '
' + active.map(r => renderRecurringCard(r)).join('') + '
'; } if (inactive.length > 0) { html += '

' + window.getTranslation('recurring.inactive', 'Inactive') + '

'; html += '
' + inactive.map(r => renderRecurringCard(r)).join('') + '
'; } container.innerHTML = html; } // Render individual recurring expense card function renderRecurringCard(recurring) { const nextDue = new Date(recurring.next_due_date); const today = new Date(); const daysUntil = Math.ceil((nextDue - today) / (1000 * 60 * 60 * 24)); let dueDateClass = 'text-text-muted dark:text-[#92adc9]'; let dueDateText = ''; if (daysUntil < 0) { dueDateClass = 'text-red-400'; dueDateText = window.getTranslation('recurring.overdue', 'Overdue'); } else if (daysUntil === 0) { dueDateClass = 'text-orange-400'; dueDateText = window.getTranslation('recurring.dueToday', 'Due today'); } else if (daysUntil <= 7) { dueDateClass = 'text-yellow-400'; dueDateText = window.getTranslation('recurring.dueIn', 'Due in') + ` ${daysUntil} ` + (daysUntil === 1 ? window.getTranslation('recurring.day', 'day') : window.getTranslation('recurring.days', 'days')); } else { dueDateText = nextDue.toLocaleDateString(); } const frequencyText = window.getTranslation(`recurring.frequency.${recurring.frequency}`, recurring.frequency); const autoCreateBadge = recurring.auto_create ? ` check_circle ${window.getTranslation('recurring.autoCreate', 'Auto-create')} ` : ''; const detectedBadge = recurring.detected ? ` auto_awesome ${window.getTranslation('recurring.detected', 'Auto-detected')} ${Math.round(recurring.confidence_score)}% ` : ''; return `
repeat

${recurring.name}

${autoCreateBadge} ${detectedBadge}
${recurring.category_name} ${frequencyText} ${recurring.notes ? `${recurring.notes}` : ''}
schedule ${dueDateText}
${formatCurrency(recurring.amount, window.userCurrency || recurring.currency)}
${recurring.last_created_date ? `
check_circle ${window.getTranslation('recurring.lastCreated', 'Last created')}: ${new Date(recurring.last_created_date).toLocaleDateString()}
` : ''}
${daysUntil <= 7 && recurring.is_active ? ` ` : ''}
`; } // Create expense from recurring async function createExpenseFromRecurring(recurringId) { try { const data = await apiCall(`/api/recurring/${recurringId}/create-expense`, { method: 'POST' }); showToast(window.getTranslation('recurring.expenseCreated', 'Expense created successfully!'), 'success'); loadRecurringExpenses(); } catch (error) { console.error('Failed to create expense:', error); showToast(window.getTranslation('recurring.errorCreating', 'Failed to create expense'), 'error'); } } // Toggle recurring active status async function toggleRecurringActive(recurringId, isActive) { try { await apiCall(`/api/recurring/${recurringId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_active: isActive }) }); const statusText = isActive ? window.getTranslation('recurring.activated', 'Recurring expense activated') : window.getTranslation('recurring.deactivated', 'Recurring expense deactivated'); showToast(statusText, 'success'); loadRecurringExpenses(); } catch (error) { console.error('Failed to toggle recurring status:', error); showToast(window.getTranslation('common.error', 'An error occurred'), 'error'); } } // Delete recurring expense async function deleteRecurring(recurringId) { const confirmText = window.getTranslation('recurring.deleteConfirm', 'Are you sure you want to delete this recurring expense?'); if (!confirm(confirmText)) return; try { await apiCall(`/api/recurring/${recurringId}`, { method: 'DELETE' }); showToast(window.getTranslation('recurring.deleted', 'Recurring expense deleted'), 'success'); loadRecurringExpenses(); } catch (error) { console.error('Failed to delete recurring expense:', error); showToast(window.getTranslation('common.error', 'An error occurred'), 'error'); } } // Edit recurring expense function editRecurring(recurringId) { const recurring = currentRecurring.find(r => r.id === recurringId); if (!recurring) return; // Populate form document.getElementById('recurring-id').value = recurring.id; document.getElementById('recurring-name').value = recurring.name; document.getElementById('recurring-amount').value = recurring.amount; document.getElementById('recurring-category').value = recurring.category_id; document.getElementById('recurring-frequency').value = recurring.frequency; document.getElementById('recurring-day').value = recurring.day_of_period || ''; document.getElementById('recurring-next-due').value = recurring.next_due_date.split('T')[0]; document.getElementById('recurring-auto-create').checked = recurring.auto_create; document.getElementById('recurring-notes').value = recurring.notes || ''; // Update modal title document.getElementById('modal-title').textContent = window.getTranslation('recurring.edit', 'Edit Recurring Expense'); document.getElementById('recurring-submit-btn').textContent = window.getTranslation('actions.update', 'Update'); // Show modal document.getElementById('add-recurring-modal').classList.remove('hidden'); } // Show add recurring modal function showAddRecurringModal() { document.getElementById('recurring-form').reset(); document.getElementById('recurring-id').value = ''; document.getElementById('modal-title').textContent = window.getTranslation('recurring.add', 'Add Recurring Expense'); document.getElementById('recurring-submit-btn').textContent = window.getTranslation('actions.save', 'Save'); document.getElementById('add-recurring-modal').classList.remove('hidden'); } // Close modal function closeRecurringModal() { document.getElementById('add-recurring-modal').classList.add('hidden'); } // Save recurring expense async function saveRecurringExpense(event) { event.preventDefault(); const recurringId = document.getElementById('recurring-id').value; const formData = { name: document.getElementById('recurring-name').value, amount: parseFloat(document.getElementById('recurring-amount').value), // Don't send currency - let backend use current_user.currency from settings category_id: parseInt(document.getElementById('recurring-category').value), frequency: document.getElementById('recurring-frequency').value, day_of_period: parseInt(document.getElementById('recurring-day').value) || null, next_due_date: document.getElementById('recurring-next-due').value, auto_create: document.getElementById('recurring-auto-create').checked, notes: document.getElementById('recurring-notes').value }; try { if (recurringId) { // Update await apiCall(`/api/recurring/${recurringId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }); showToast(window.getTranslation('recurring.updated', 'Recurring expense updated'), 'success'); } else { // Create await apiCall('/api/recurring/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }); showToast(window.getTranslation('recurring.created', 'Recurring expense created'), 'success'); } closeRecurringModal(); loadRecurringExpenses(); } catch (error) { console.error('Failed to save recurring expense:', error); showToast(window.getTranslation('common.error', 'An error occurred'), 'error'); } } // Detect recurring patterns async function detectRecurringPatterns() { const detectBtn = document.getElementById('detect-btn'); const originalText = detectBtn.innerHTML; detectBtn.innerHTML = 'refresh ' + window.getTranslation('recurring.detecting', 'Detecting...'); detectBtn.disabled = true; try { const data = await apiCall('/api/recurring/detect', { method: 'POST' }); detectedSuggestions = data.suggestions || []; if (detectedSuggestions.length === 0) { showToast(window.getTranslation('recurring.noPatterns', 'No recurring patterns detected'), 'info'); } else { displaySuggestions(detectedSuggestions); document.getElementById('suggestions-section').classList.remove('hidden'); showToast(window.getTranslation('recurring.patternsFound', `Found ${detectedSuggestions.length} potential recurring expenses`), 'success'); } } catch (error) { console.error('Failed to detect patterns:', error); showToast(window.getTranslation('recurring.errorDetecting', 'Failed to detect patterns'), 'error'); } finally { detectBtn.innerHTML = originalText; detectBtn.disabled = false; } } // Display suggestions function displaySuggestions(suggestions) { const container = document.getElementById('suggestions-list'); container.innerHTML = suggestions.map((s, index) => `
auto_awesome

${s.name}

${s.category_name} ${window.getTranslation(`recurring.frequency.${s.frequency}`, s.frequency)} ${s.occurrences} ${window.getTranslation('recurring.occurrences', 'occurrences')}
${formatCurrency(s.amount, window.userCurrency || s.currency)}
verified ${Math.round(s.confidence_score)}% ${window.getTranslation('recurring.confidence', 'confidence')}
`).join(''); } // Accept suggestion async function acceptSuggestion(index) { const suggestion = detectedSuggestions[index]; try { await apiCall('/api/recurring/accept-suggestion', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(suggestion) }); showToast(window.getTranslation('recurring.suggestionAccepted', 'Recurring expense added'), 'success'); // Remove suggestion detectedSuggestions.splice(index, 1); if (detectedSuggestions.length === 0) { document.getElementById('suggestions-section').classList.add('hidden'); } else { displaySuggestions(detectedSuggestions); } loadRecurringExpenses(); } catch (error) { console.error('Failed to accept suggestion:', error); showToast(window.getTranslation('common.error', 'An error occurred'), 'error'); } } // Dismiss suggestion function dismissSuggestion(index) { detectedSuggestions.splice(index, 1); if (detectedSuggestions.length === 0) { document.getElementById('suggestions-section').classList.add('hidden'); } else { displaySuggestions(detectedSuggestions); } } // Load categories for dropdown async function loadCategories() { try { const data = await apiCall('/api/expenses/categories'); const select = document.getElementById('recurring-category'); select.innerHTML = data.categories.map(cat => `` ).join(''); } catch (error) { console.error('Failed to load categories:', error); } } // Update day field based on frequency function updateDayField() { const frequency = document.getElementById('recurring-frequency').value; const dayContainer = document.getElementById('day-container'); const dayInput = document.getElementById('recurring-day'); const dayLabel = document.getElementById('day-label'); if (frequency === 'weekly') { dayContainer.classList.remove('hidden'); dayLabel.textContent = window.getTranslation('recurring.dayOfWeek', 'Day of week'); dayInput.type = 'select'; dayInput.innerHTML = ` `; } else if (frequency === 'monthly') { dayContainer.classList.remove('hidden'); dayLabel.textContent = window.getTranslation('recurring.dayOfMonth', 'Day of month'); dayInput.type = 'number'; dayInput.min = '1'; dayInput.max = '28'; } else { dayContainer.classList.add('hidden'); } } // Initialize document.addEventListener('DOMContentLoaded', async () => { if (document.getElementById('recurring-list')) { await loadUserCurrency(); // Sync all recurring expenses to user's current currency await syncRecurringCurrency(); loadRecurringExpenses(); loadCategories(); // Set default next due date to tomorrow const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); document.getElementById('recurring-next-due').valueAsDate = tomorrow; // Event listeners document.getElementById('recurring-form')?.addEventListener('submit', saveRecurringExpense); document.getElementById('recurring-frequency')?.addEventListener('change', updateDayField); } }); // Sync recurring expenses currency with user profile async function syncRecurringCurrency() { try { await apiCall('/api/recurring/sync-currency', { method: 'POST' }); } catch (error) { console.error('Failed to sync currency:', error); } }