Import existing project
This commit is contained in:
parent
7887817595
commit
80b0cc4939
125 changed files with 16980 additions and 0 deletions
858
develop/js/cc.js
Normal file
858
develop/js/cc.js
Normal file
|
|
@ -0,0 +1,858 @@
|
|||
/**
|
||||
* 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 = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Payment History</h2>
|
||||
<button class="close-modal" id="close-history-modal">×</button>
|
||||
</div>
|
||||
<div id="payment-history-container">
|
||||
<!-- Payment history will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="card-header">
|
||||
<div class="card-details">
|
||||
<h2 class="card-title">${card.name}</h2>
|
||||
<div class="card-bank">${card.bank}${card.lastDigits ? ` •••• ${card.lastDigits}` : ''}</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button type="button" class="action-btn payment-btn" title="Record Payment" aria-label="Record Payment">💰</button>
|
||||
<button type="button" class="action-btn edit-btn" title="Edit Card" aria-label="Edit Card">✏️</button>
|
||||
<button type="button" class="action-btn delete-btn" title="Delete Card" aria-label="Delete Card">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="info-row">
|
||||
<span class="info-label">Statement Cycle:</span>
|
||||
<span class="info-value">Day ${card.statementDate}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Cycle Resets:</span>
|
||||
<span class="info-value">${daysUntilReset === 0 ? 'Today' : `In ${daysUntilReset} day${daysUntilReset !== 1 ? 's' : ''}`}</span>
|
||||
</div>
|
||||
${card.expiryDate ? `
|
||||
<div class="info-row">
|
||||
<span class="info-label">Expires:</span>
|
||||
<span class="info-value">${formatExpiryDate(card.expiryDate)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="rewards-categories">
|
||||
${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 `
|
||||
<div class="category-item ${hasPayments ? 'has-payments' : ''}" data-category-name="${category.name}">
|
||||
<div class="category-header">
|
||||
<span class="category-tag">${category.name}: ${category.rate}%</span>
|
||||
${hasLimit ? `
|
||||
<span class="info-value">$${spent.toFixed(2)} / $${category.limit.toFixed(2)}${cashbackDisplay}</span>
|
||||
` : `<span class="info-value">$${spent.toFixed(2)}${cashbackDisplay}</span>`}
|
||||
</div>
|
||||
${hasLimit ? `
|
||||
<div class="progress-bar ${isNearLimit ? 'near-limit' : ''} ${isAtLimit ? 'at-limit' : ''}">
|
||||
<div class="progress-fill" style="width: ${Math.min(percentUsed, 100)}%"></div>
|
||||
</div>
|
||||
` : ''}
|
||||
${hasPayments ? `<div class="view-payments-link">View ${payments.length} payment${payments.length !== 1 ? 's' : ''}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="empty-state">
|
||||
<h3>No payment history</h3>
|
||||
<p>No payments have been recorded for this category yet.</p>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
const sortedPayments = [...payments].sort((a, b) =>
|
||||
new Date(b.date) - new Date(a.date)
|
||||
);
|
||||
|
||||
const historyHtml = `
|
||||
<div class="payment-history-list">
|
||||
<div class="payment-history-header">
|
||||
<span>Date</span>
|
||||
<span>Amount</span>
|
||||
<span>Note</span>
|
||||
<span>Actions</span>
|
||||
</div>
|
||||
${sortedPayments.map(payment => `
|
||||
<div class="payment-history-item" data-payment-id="${payment.id}">
|
||||
<span class="payment-date">${formatDate(payment.date)}</span>
|
||||
<span class="payment-amount">$${parseFloat(payment.amount).toFixed(2)}</span>
|
||||
<span class="payment-note">${payment.note || '-'}</span>
|
||||
<div class="payment-actions">
|
||||
<button type="button" class="action-btn edit-payment-btn" title="Edit Payment" aria-label="Edit Payment">✏️</button>
|
||||
<button type="button" class="action-btn delete-payment-btn" title="Delete Payment" aria-label="Delete Payment">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<input type="text" class="form-input category-name" placeholder="Category (e.g. Dining)" value="${name}">
|
||||
<input type="number" class="form-input category-rate" placeholder="Rate %" step="0.1" min="0" value="${rate}">
|
||||
<input type="number" class="form-input category-limit" placeholder="Limit $" step="0.01" min="0" value="${limit}">
|
||||
<button type="button" class="action-btn delete-btn remove-category" title="Remove Category" aria-label="Remove Category">×</button>
|
||||
`;
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in a new issue