/** * Credit Card Tracker * Tracks reward categories, spending limits, and payment history for credit cards */ document.addEventListener('DOMContentLoaded', () => { // DOM Elements const cardsContainer = document.getElementById('cards-container'); const emptyState = document.getElementById('empty-state'); const searchInput = document.getElementById('search-input'); const addCardBtn = document.getElementById('add-card-btn'); const addFirstCardBtn = document.getElementById('add-first-card-btn'); const cardModal = document.getElementById('card-modal'); const closeModalBtn = document.getElementById('close-modal'); const cardForm = document.getElementById('card-form'); const modalTitle = document.getElementById('modal-title'); const cardIdInput = document.getElementById('card-id'); const addCategoryBtn = document.getElementById('add-category-btn'); const categoriesContainer = document.getElementById('categories-container'); // Payment Modal Elements const paymentModal = document.getElementById('payment-modal'); const closePaymentModalBtn = document.getElementById('close-payment-modal'); const paymentForm = document.getElementById('payment-form'); const paymentCardId = document.getElementById('payment-card-id'); const paymentCardName = document.getElementById('payment-card-name'); const paymentCategory = document.getElementById('payment-category'); const paymentAmount = document.getElementById('payment-amount'); const paymentDate = document.getElementById('payment-date'); // Create Payment History Modal - Will be added to DOM later const paymentHistoryModal = document.createElement('div'); paymentHistoryModal.className = 'modal'; paymentHistoryModal.id = 'payment-history-modal'; // Initialize cards from localStorage let cards = loadCards(); // Display cards or empty state renderCards(); // Set today's date as default for payment date paymentDate.valueAsDate = new Date(); // Event Listeners addCardBtn.addEventListener('click', () => openAddCardModal()); addFirstCardBtn.addEventListener('click', () => openAddCardModal()); closeModalBtn.addEventListener('click', () => closeModal(cardModal)); closePaymentModalBtn.addEventListener('click', () => closeModal(paymentModal)); cardForm.addEventListener('submit', handleCardFormSubmit); addCategoryBtn.addEventListener('click', () => addCategoryField('', '', '')); paymentForm.addEventListener('submit', handlePaymentFormSubmit); searchInput.addEventListener('input', handleSearch); // Global event delegation for dynamically added card buttons cardsContainer.addEventListener('click', handleCardActions); // Check for monthly resets on page load checkMonthlyResets(); // Initialize payment history modal initPaymentHistoryModal(); /** * Initialize payment history modal */ function initPaymentHistoryModal() { paymentHistoryModal.innerHTML = ` `; document.body.appendChild(paymentHistoryModal); // Add event listener to close button document.getElementById('close-history-modal').addEventListener('click', () => { closeModal(paymentHistoryModal); }); } /** * Handle card action buttons via event delegation */ function handleCardActions(e) { const target = e.target; // Find closest card element const cardElement = target.closest('.credit-card'); if (!cardElement) return; const cardId = cardElement.dataset.id; const card = cards.find(c => c.id === cardId); if (!card) return; // Payment button if (target.closest('.payment-btn')) { e.stopPropagation(); openPaymentModal(card); return; } // Edit button if (target.closest('.edit-btn')) { e.stopPropagation(); openEditCardModal(card); return; } // Delete button if (target.closest('.delete-btn')) { e.stopPropagation(); deleteCard(cardId); return; } // View payment history (clicking on a category item) const categoryItem = target.closest('.category-item'); if (categoryItem && categoryItem.dataset.categoryName) { const categoryName = categoryItem.dataset.categoryName; const category = card.categories.find(c => c.name === categoryName); if (category) { openPaymentHistoryModal(card, category); } } } /** * Load cards from localStorage */ function loadCards() { const storedCards = localStorage.getItem('creditCards'); return storedCards ? JSON.parse(storedCards) : []; } /** * Save cards to localStorage */ function saveCards() { localStorage.setItem('creditCards', JSON.stringify(cards)); } /** * Render all cards or empty state */ function renderCards() { // Clear cards container except for the empty state Array.from(cardsContainer.children).forEach(child => { if (!child.classList.contains('empty-state')) { child.remove(); } }); // Show empty state if no cards if (cards.length === 0) { emptyState.style.display = 'block'; return; } // Hide empty state and render cards emptyState.style.display = 'none'; // Filter cards if search input has value let filteredCards = cards; const searchTerm = searchInput.value.toLowerCase().trim(); if (searchTerm) { filteredCards = cards.filter(card => { const nameMatch = card.name.toLowerCase().includes(searchTerm); const bankMatch = card.bank.toLowerCase().includes(searchTerm); const categoryMatch = card.categories.some(cat => cat.name.toLowerCase().includes(searchTerm) ); return nameMatch || bankMatch || categoryMatch; }); } // Render filtered cards filteredCards.forEach(card => { const cardElement = createCardElement(card); cardsContainer.insertBefore(cardElement, emptyState); }); } /** * Create a card element */ function createCardElement(card) { const cardElement = document.createElement('div'); cardElement.className = 'credit-card'; cardElement.dataset.id = card.id; // Calculate days until cycle resets const today = new Date(); const currentDay = today.getDate(); const cycleDay = parseInt(card.statementDate); let daysUntilReset; if (currentDay === cycleDay) { daysUntilReset = 0; } else if (currentDay < cycleDay) { daysUntilReset = cycleDay - currentDay; } else { // Calculate days until next month's cycle date const lastDayOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate(); daysUntilReset = (lastDayOfMonth - currentDay) + cycleDay; } // Create card HTML cardElement.innerHTML = `

${card.name}

${card.bank}${card.lastDigits ? ` •••• ${card.lastDigits}` : ''}
Statement Cycle: Day ${card.statementDate}
Cycle Resets: ${daysUntilReset === 0 ? 'Today' : `In ${daysUntilReset} day${daysUntilReset !== 1 ? 's' : ''}`}
${card.expiryDate ? `
Expires: ${formatExpiryDate(card.expiryDate)}
` : ''}
${card.categories.map(category => { const payments = category.payments; // Assumed to be an array now const spent = payments.reduce((total, p) => total + parseFloat(p.amount), 0); const hasLimit = category.limit > 0; const percentUsed = hasLimit ? (spent / category.limit) * 100 : 0; const isNearLimit = percentUsed >= 75 && percentUsed < 100; const isAtLimit = percentUsed >= 100; const hasPayments = payments.length > 0; // Calculate cash back amounts const cashbackEarned = (spent * category.rate / 100).toFixed(2); const maxCashback = hasLimit ? (category.limit * category.rate / 100).toFixed(2) : 0; const cashbackDisplay = hasLimit ? ` ($${cashbackEarned}/$${maxCashback})` : ` ($${cashbackEarned})`; return `
${category.name}: ${category.rate}% ${hasLimit ? ` $${spent.toFixed(2)} / $${category.limit.toFixed(2)}${cashbackDisplay} ` : `$${spent.toFixed(2)}${cashbackDisplay}`}
${hasLimit ? `
` : ''} ${hasPayments ? `` : ''}
`; }).join('')}
`; return cardElement; } /** * Format expiry date (YYYY-MM to MM/YYYY) */ function formatExpiryDate(dateString) { const [year, month] = dateString.split('-'); return `${month}/${year}`; } /** * Format a date for display (YYYY-MM-DD to MM/DD/YYYY) */ function formatDate(dateString) { const date = new Date(dateString); return date.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' }); } /** * Open modal to add a new card */ function openAddCardModal() { // Reset form cardForm.reset(); cardIdInput.value = ''; modalTitle.textContent = 'Add New Card'; // Clear existing category fields and add one empty one clearCategoryFields(); // Open modal openModal(cardModal); } /** * Open modal to edit an existing card */ function openEditCardModal(card) { // Set form values cardForm.reset(); cardIdInput.value = card.id; document.getElementById('card-name').value = card.name; document.getElementById('card-bank').value = card.bank; document.getElementById('last-digits').value = card.lastDigits || ''; document.getElementById('expiry-date').value = card.expiryDate || ''; document.getElementById('statement-date').value = card.statementDate; modalTitle.textContent = 'Edit Card'; // Clear existing category fields clearCategoryFields(); // Add category fields for existing categories if (card.categories.length === 0) { } else { card.categories.forEach(category => { addCategoryField(category.name, category.rate, category.limit); }); } // Open modal openModal(cardModal); } /** * Open modal to record a payment */ function openPaymentModal(card) { paymentCardId.value = card.id; paymentCardName.textContent = card.name; // Clear and populate category dropdown paymentCategory.innerHTML = ''; card.categories.forEach(category => { if (!category) return; const option = document.createElement('option'); option.value = category.name; option.textContent = `${category.name} (${category.rate}%)`; // Add info about limit if one exists if (category.limit > 0) { const payments = category.payments; // Assumed to be an array now const spent = payments.reduce((total, p) => total + parseFloat(p.amount), 0); const remaining = Math.max(0, category.limit - spent); option.textContent += ` - $${remaining.toFixed(2)} remaining`; } paymentCategory.appendChild(option); }); // Reset other fields paymentAmount.value = ''; paymentDate.valueAsDate = new Date(); document.getElementById('payment-note').value = ''; // Set modal title and button text for new payment paymentModal.querySelector('.modal-title').textContent = 'Record Payment'; paymentModal.querySelector('.save-btn').textContent = 'Record Payment'; // Open modal openModal(paymentModal); } /** * Open payment history modal for a category */ function openPaymentHistoryModal(card, category) { if (!card || !category) return; const container = document.getElementById('payment-history-container'); const modalTitle = paymentHistoryModal.querySelector('.modal-title'); modalTitle.textContent = `${card.name} - ${category.name} Payments`; container.innerHTML = ''; // Clear previous content const payments = category.payments; // Assumed to be an array now if (payments.length === 0) { container.innerHTML = `

No payment history

No payments have been recorded for this category yet.

`; } else { const sortedPayments = [...payments].sort((a, b) => new Date(b.date) - new Date(a.date) ); const historyHtml = `
Date Amount Note Actions
${sortedPayments.map(payment => `
${formatDate(payment.date)} $${parseFloat(payment.amount).toFixed(2)} ${payment.note || '-'}
`).join('')}
`; container.innerHTML = historyHtml; // Add event listeners for edit and delete payment buttons container.querySelectorAll('.edit-payment-btn').forEach(btn => { btn.addEventListener('click', e => { const paymentId = e.target.closest('.payment-history-item').dataset.paymentId; const payment = payments.find(p => p.id === paymentId); if (payment) { editPayment(card.id, category.name, payment); } }); }); container.querySelectorAll('.delete-payment-btn').forEach(btn => { btn.addEventListener('click', e => { const paymentId = e.target.closest('.payment-history-item').dataset.paymentId; const payment = payments.find(p => p.id === paymentId); if (payment) { deletePayment(card.id, category.name, payment.id); } }); }); } // Open the modal openModal(paymentHistoryModal); } /** * Edit a payment */ function editPayment(cardId, categoryName, payment) { // Set up payment modal for editing paymentCardId.value = cardId; const card = cards.find(c => c.id === cardId); if (!card) return; paymentCardName.textContent = card.name; // Populate category dropdown (with only the selected category) paymentCategory.innerHTML = ''; const category = card.categories.find(c => c.name === categoryName); if (!category) return; const option = document.createElement('option'); option.value = category.name; option.textContent = `${category.name} (${category.rate}%)`; paymentCategory.appendChild(option); // Set payment details paymentAmount.value = payment.amount; paymentDate.value = payment.date; document.getElementById('payment-note').value = payment.note || ''; // Add payment ID to the form const paymentIdInput = document.getElementById('payment-id') || document.createElement('input'); paymentIdInput.type = 'hidden'; paymentIdInput.id = 'payment-id'; paymentIdInput.value = payment.id; if (!document.getElementById('payment-id')) { paymentForm.appendChild(paymentIdInput); } // Change modal title and button text paymentModal.querySelector('.modal-title').textContent = 'Edit Payment'; paymentModal.querySelector('.save-btn').textContent = 'Update Payment'; // Close history modal and open payment modal with a slight delay for better transition closeModal(paymentHistoryModal); setTimeout(() => { openModal(paymentModal); }, 300); } /** * Delete a payment */ function deletePayment(cardId, categoryName, paymentId) { if (!confirm('Are you sure you want to delete this payment?')) return; const cardIndex = cards.findIndex(c => c.id === cardId); if (cardIndex === -1) return; const categoryIndex = cards[cardIndex].categories.findIndex(c => c.name === categoryName); if (categoryIndex === -1) return; // Remove the payment from the array const payments = cards[cardIndex].categories[categoryIndex].payments; // Assumed to be array const paymentIndex = payments.findIndex(p => p.id === paymentId); if (paymentIndex === -1) return; // Get payment amount for confirmation const amount = payments[paymentIndex].amount; // Remove the payment payments.splice(paymentIndex, 1); // Save changes saveCards(); // Close the history modal closeModal(paymentHistoryModal); // Show confirmation toast showToast(`Payment of $${amount.toFixed(2)} has been deleted`); // Re-render cards renderCards(); } /** * Open a modal */ function openModal(modal) { modal.classList.add('active'); document.body.style.overflow = 'hidden'; // Prevent background scrolling // Focus first input in modal for better accessibility setTimeout(() => { const firstInput = modal.querySelector('input:not([type="hidden"])'); if (firstInput) firstInput.focus(); }, 100); } /** * Close a modal */ function closeModal(modal) { modal.classList.remove('active'); document.body.style.overflow = ''; // Restore scrolling } /** * Clear all category fields */ function clearCategoryFields() { categoriesContainer.innerHTML = ''; } /** * Add a category field to the form */ function addCategoryField(name = '', rate = '', limit = '') { const categoryField = document.createElement('div'); categoryField.className = 'category-inputs'; categoryField.innerHTML = ` `; // Add event listener to remove button categoryField.querySelector('.remove-category').addEventListener('click', function() { // Allow removing the last category field categoryField.remove(); }); categoriesContainer.appendChild(categoryField); } /** * Handle card form submission */ function handleCardFormSubmit(e) { e.preventDefault(); // Get form values const cardId = cardIdInput.value || generateId(); const isEditing = !!cardIdInput.value; const name = document.getElementById('card-name').value; const bank = document.getElementById('card-bank').value; const lastDigits = document.getElementById('last-digits').value; const expiryDate = document.getElementById('expiry-date').value; const statementDate = document.getElementById('statement-date').value; // Get categories const categories = []; const categoryInputs = categoriesContainer.querySelectorAll('.category-inputs'); categoryInputs.forEach(input => { const catName = input.querySelector('.category-name').value.trim(); const catRate = input.querySelector('.category-rate').value.trim(); const catLimit = input.querySelector('.category-limit').value.trim(); // Only add this category if any reward detail is provided if (!catName && !catRate && !catLimit) { return; } // Determine payments: Use existing if editing and found, otherwise default to empty array let payments = []; const existingCardData = isEditing ? cards.find(c => c.id === cardId) : null; if (isEditing && existingCardData && existingCardData.categories) { const existingCategoryData = existingCardData.categories.find(c => c.name === catName); if (existingCategoryData && Array.isArray(existingCategoryData.payments)) { payments = existingCategoryData.payments; } } categories.push({ name: catName, rate: catRate ? parseFloat(catRate) : 0, limit: catLimit ? parseFloat(catLimit) : null, payments: payments // Ensured to be an array }); }); // Create card object const card = { id: cardId, name, bank, lastDigits, expiryDate, statementDate, categories, // categories array now guaranteed to have .payments arrays createdAt: new Date().toISOString() }; // Update or add card const existingCardIndex = isEditing ? cards.findIndex(c => c.id === cardId) : -1; if (existingCardIndex !== -1) { // Preserve archived payments if editing card.archivedPayments = cards[existingCardIndex].archivedPayments; cards[existingCardIndex] = card; } else { cards.push(card); } // Save and render cards saveCards(); renderCards(); // Show feedback toast showToast(isEditing ? 'Card updated successfully' : 'Card added successfully'); // Close modal closeModal(cardModal); } /** * Handle payment form submission */ function handlePaymentFormSubmit(e) { e.preventDefault(); // Get form values const cardId = paymentCardId.value; const categoryName = paymentCategory.value; const amount = parseFloat(paymentAmount.value); const date = paymentDate.value; const note = document.getElementById('payment-note').value; const paymentId = document.getElementById('payment-id')?.value; // Find card and category const cardIndex = cards.findIndex(c => c.id === cardId); if (cardIndex === -1) return; const categoryIndex = cards[cardIndex].categories.findIndex(c => c.name === categoryName); if (categoryIndex === -1) return; // Check if editing existing payment or adding new one const payments = cards[cardIndex].categories[categoryIndex].payments; // Assumed to be array if (paymentId) { // Find the payment const paymentIndex = payments.findIndex(p => p.id === paymentId); if (paymentIndex !== -1) { // Update payment payments[paymentIndex] = { id: paymentId, amount, date, note, createdAt: payments[paymentIndex].createdAt, // Keep original creation date updatedAt: new Date().toISOString() }; // Show toast showToast('Payment updated successfully'); } } else { // Add new payment const payment = { id: generateId(), amount, date, note, createdAt: new Date().toISOString() }; payments.push(payment); // Show toast showToast(`Payment of $${amount.toFixed(2)} recorded`); } // Save and render cards saveCards(); renderCards(); // Remove payment ID if it exists if (document.getElementById('payment-id')) { document.getElementById('payment-id').remove(); } // Reset modal title paymentModal.querySelector('.modal-title').textContent = 'Record Payment'; // Close modal closeModal(paymentModal); } /** * Delete a card */ function deleteCard(cardId) { if (!confirm('Are you sure you want to delete this card?')) return; const cardName = cards.find(card => card.id === cardId)?.name || 'Card'; cards = cards.filter(card => card.id !== cardId); saveCards(); renderCards(); // Show feedback toast showToast(`${cardName} has been deleted`); } /** * Show a toast notification */ function showToast(message) { // Remove existing toast if any const existingToast = document.querySelector('.toast'); if (existingToast) { existingToast.remove(); } // Create toast element const toast = document.createElement('div'); toast.className = 'toast'; toast.textContent = message; document.body.appendChild(toast); // Trigger animation setTimeout(() => toast.classList.add('show'), 10); // Auto remove after 3 seconds setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 3000); } /** * Handle search */ function handleSearch() { renderCards(); } /** * Generate a unique ID */ function generateId() { return Date.now().toString(36) + Math.random().toString(36).substr(2, 5); } /** * Check for monthly cycle resets */ function checkMonthlyResets() { const today = new Date(); const lastCheck = localStorage.getItem('lastCycleCheck'); // If we haven't checked today if (!lastCheck || new Date(lastCheck).toDateString() !== today.toDateString()) { const currentDay = today.getDate(); // Check each card for cycle reset cards.forEach(card => { const cycleDay = parseInt(card.statementDate); // If today is the cycle reset day if (currentDay === cycleDay) { // Mark payments as from previous cycle card.categories.forEach(category => { // Only archive if there are payments if (category.payments.length > 0) { // Create an archive if none exists if (!card.archivedPayments) { card.archivedPayments = []; } // Archive current cycle payments const cycleData = { date: today.toISOString(), categories: [{ name: category.name, rate: category.rate, payments: [...category.payments] }] }; // Add to archived payments card.archivedPayments.push(cycleData); // Clear current payments category.payments = []; } }); } }); // Save changes saveCards(); // Update last check date localStorage.setItem('lastCycleCheck', today.toISOString()); } } });