1
0
Fork 0
This repository has been archived on 2025-05-26. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
Checkpoint-Golang/develop/js/cc.js
2025-05-26 12:42:36 -05:00

858 lines
No EOL
28 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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">&times;</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());
}
}
});