diff --git a/app.js b/app.js
index fc9ec6a..a497b6f 100644
--- a/app.js
+++ b/app.js
@@ -184,7 +184,7 @@
list.innerHTML = '';
categories.sort((a,b) => a.name.localeCompare(b.name)).forEach(cat => {
const el = document.createElement('div');
- el.className = 'flex justify-between items-center p-2 bg-white rounded-lg shadow-sm';
+ el.className = 'flex items-center justify-between rounded-xl bg-white/80 px-4 py-3 text-sm shadow-sm backdrop-blur transition hover:-translate-y-0.5 hover:shadow-lg dark:bg-slate-800/70 dark:text-slate-100';
el.innerHTML = `
${cat.name} (${cat.type})
@@ -213,7 +213,7 @@
list.innerHTML = '';
vendors.forEach(v => {
const el = document.createElement('div');
- el.className = 'flex justify-between items-center p-2 bg-white rounded-lg shadow-sm';
+ el.className = 'flex items-center justify-between rounded-xl bg-white/80 px-4 py-3 text-sm shadow-sm backdrop-blur transition hover:-translate-y-0.5 hover:shadow-lg dark:bg-slate-800/70 dark:text-slate-100';
el.innerHTML = `
${v.name}
@@ -242,7 +242,7 @@
list.innerHTML = '';
rules.forEach(rule => {
const el = document.createElement('div');
- el.className = 'flex justify-between items-center p-2 bg-white rounded-lg shadow-sm';
+ el.className = 'flex items-center justify-between gap-3 rounded-xl bg-white/80 px-4 py-3 text-sm shadow-sm backdrop-blur transition hover:-translate-y-0.5 hover:shadow-lg dark:bg-slate-800/70 dark:text-slate-100';
el.innerHTML = `
If description contains "${rule.keyword}", set category to ${rule.category}
@@ -254,9 +254,15 @@
function renderTransactions() {
const tbody = document.getElementById('transactionsTableBody');
- const filter = document.getElementById('transactionFilter').value;
- const search = document.getElementById('transactionSearch').value.toLowerCase();
- if (!tbody) return;
+ const mobileList = document.getElementById('transactionsListMobile');
+ const filterEl = document.getElementById('transactionFilter');
+ const searchEl = document.getElementById('transactionSearch');
+ const mobileSortSelect = document.getElementById('mobileSortSelect');
+
+ if (!tbody && !mobileList) return;
+
+ const filter = filterEl ? filterEl.value : 'all';
+ const search = searchEl ? searchEl.value.toLowerCase() : '';
let txsToRender = [...transactions];
@@ -296,75 +302,146 @@
return 0;
});
- // 2. Filter by Category Status
+ // 3. Filter by Category Status
if (filter === 'uncategorized') {
txsToRender = txsToRender.filter(tx => tx.reconciliation_status === 'uncategorized');
}
- // 3. Filter by Search
+ // 4. Filter by Search
if (search) {
txsToRender = txsToRender.filter(tx => tx.description.toLowerCase().includes(search));
}
- // 4. Update Header styles
+ // 5. Update Header styles + mobile select
document.querySelectorAll('.sortable-header').forEach(th => {
th.classList.remove('sort-asc', 'sort-desc');
if (th.dataset.sort === transactionSort.column) {
th.classList.add(transactionSort.direction === 'asc' ? 'sort-asc' : 'sort-desc');
}
});
+ if (mobileSortSelect) {
+ const desiredValue = `${transactionSort.column}-${transactionSort.direction}`;
+ if (Array.from(mobileSortSelect.options).some(opt => opt.value === desiredValue)) {
+ mobileSortSelect.value = desiredValue;
+ }
+ }
- // 5. Render
- tbody.innerHTML = '';
+ // 6. Render
+ if (tbody) tbody.innerHTML = '';
+ if (mobileList) mobileList.innerHTML = '';
+
if (txsToRender.length === 0) {
- tbody.innerHTML = '
| No transactions found. |
';
+ if (tbody) {
+ tbody.innerHTML = '| No transactions found. |
';
+ }
+ if (mobileList) {
+ const emptyCard = document.createElement('div');
+ emptyCard.className = 'card text-center text-sm text-slate-500 dark:text-slate-300';
+ emptyCard.textContent = 'No transactions found.';
+ mobileList.appendChild(emptyCard);
+ }
return;
}
txsToRender.forEach(tx => {
const txDate = new Date(tx.date);
- const tr = document.createElement('tr');
- tr.className = 'border-b hover:bg-gray-50';
-
- // Determine Category/Vendor display
+
let categoryDisplay = '';
+ let categoryLabel = '';
let vendorDisplay = '';
+ let vendorLabel = '';
+
if (tx.reconciliation_status === 'uncategorized') {
- categoryDisplay = 'Uncategorized';
+ categoryDisplay = 'Uncategorized';
+ categoryLabel = 'Uncategorized';
vendorDisplay = 'N/A';
+ vendorLabel = 'N/A';
} else if (tx.split_count === 1) {
categoryDisplay = tx.category || 'N/A';
+ categoryLabel = tx.category || 'N/A';
vendorDisplay = tx.vendorName || 'N/A';
+ vendorLabel = tx.vendorName || 'N/A';
} else if (tx.split_count > 1) {
categoryDisplay = '— Split —';
+ categoryLabel = 'Split';
vendorDisplay = '— Split —';
- } else { // split_count is 0 or null
- categoryDisplay = 'Needs Attention';
+ vendorLabel = 'Split';
+ } else {
+ categoryDisplay = 'Needs Attention';
+ categoryLabel = 'Needs attention';
vendorDisplay = 'N/A';
+ vendorLabel = 'N/A';
}
- tr.innerHTML = `
- ${txDate.toLocaleDateString()} |
- ${tx.description} |
- ${categoryDisplay} |
- ${vendorDisplay} |
-
- ${tx.amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' })}
- |
-
-
-
- |
- `;
- tbody.appendChild(tr);
+ if (tbody) {
+ const tr = document.createElement('tr');
+ tr.className = 'border-b border-slate-100 transition dark:border-slate-800 hover:bg-slate-50/80 dark:hover:bg-slate-800/60';
+ tr.innerHTML = `
+ ${txDate.toLocaleDateString()} |
+ ${tx.description} |
+ ${categoryDisplay} |
+ ${vendorDisplay} |
+
+ ${tx.amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' })}
+ |
+
+
+
+ |
+ `;
+ tbody.appendChild(tr);
+ }
+
+ if (mobileList) {
+ const card = document.createElement('div');
+ card.className = 'card space-y-3';
+ const amountColor = tx.amount >= 0 ? 'text-emerald-500' : 'text-rose-500';
+ card.innerHTML = `
+
+
+
${txDate.toLocaleDateString()}
+
${tx.description}
+
+
+
${tx.amount >= 0 ? 'Incoming' : 'Outgoing'}
+
${tx.amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' })}
+
+
+
+ ${categoryLabel}
+ Vendor: ${vendorLabel}
+ ${tx.reconciliation_status}
+
+
+
+
+
+ `;
+ mobileList.appendChild(card);
+ }
});
- tbody.querySelectorAll('.edit-tx-btn').forEach(btn => {
- btn.onclick = (e) => showSplitTransactionModal(e.target.dataset.id);
- });
- tbody.querySelectorAll('.delete-tx-btn').forEach(btn => {
- btn.onclick = (e) => deleteTransaction(e.target.dataset.id);
- });
+ if (tbody) {
+ tbody.querySelectorAll('.edit-tx-btn').forEach(btn => {
+ btn.onclick = (e) => showSplitTransactionModal(e.target.dataset.id);
+ });
+ tbody.querySelectorAll('.delete-tx-btn').forEach(btn => {
+ btn.onclick = (e) => deleteTransaction(e.target.dataset.id);
+ });
+ }
+
+ if (mobileList) {
+ mobileList.querySelectorAll('.mobile-edit-tx-btn').forEach(btn => {
+ btn.onclick = (e) => showSplitTransactionModal(e.currentTarget.dataset.id);
+ });
+ mobileList.querySelectorAll('.mobile-delete-tx-btn').forEach(btn => {
+ btn.onclick = (e) => deleteTransaction(e.currentTarget.dataset.id);
+ });
+ }
}
function handleSort(e) {
@@ -1026,18 +1103,20 @@
document.querySelectorAll('main > section').forEach(section => {
section.classList.add('hidden');
});
- document.querySelectorAll('nav a').forEach(a => {
- a.classList.remove('bg-blue-700', 'text-white');
- a.classList.add('text-blue-100', 'hover:bg-blue-600');
+ document.querySelectorAll('#sidebar nav a').forEach(a => {
+ a.classList.remove('active-nav');
+ a.removeAttribute('aria-current');
});
const activeView = document.getElementById(viewId);
- const activeLink = document.querySelector(`nav a[data-view="${viewId}"]`);
+ const activeLink = document.querySelector(`#sidebar nav a[data-view="${viewId}"]`);
- if (activeView) activeView.classList.remove('hidden');
+ if (activeView) {
+ activeView.classList.remove('hidden');
+ }
if (activeLink) {
- activeLink.classList.add('bg-blue-700', 'text-white');
- activeLink.classList.remove('text-blue-100', 'hover:bg-blue-600');
+ activeLink.classList.add('active-nav');
+ activeLink.setAttribute('aria-current', 'page');
}
// Refresh dashboard data only when switching to it
@@ -1059,6 +1138,79 @@
// --- Initial App Load ---
document.addEventListener('DOMContentLoaded', () => {
+ const body = document.body;
+ const root = document.documentElement;
+ const darkModeToggle = document.getElementById('darkModeToggle');
+ const sidebar = document.getElementById('sidebar');
+ const overlay = document.getElementById('mobileOverlay');
+ const menuToggle = document.getElementById('menuToggle');
+ const closeSidebarBtn = document.getElementById('closeSidebar');
+
+ const applyTheme = (mode) => {
+ const isDark = mode === 'dark';
+ body.classList.toggle('dark-mode', isDark);
+ root.classList.toggle('dark', isDark);
+ };
+
+ const savedTheme = localStorage.getItem('simpleLedgerTheme');
+ if (savedTheme) {
+ applyTheme(savedTheme);
+ } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ applyTheme('dark');
+ }
+
+ if (menuToggle) {
+ menuToggle.setAttribute('aria-expanded', 'false');
+ }
+
+ if (darkModeToggle) {
+ darkModeToggle.addEventListener('click', () => {
+ const isDark = !body.classList.contains('dark-mode');
+ applyTheme(isDark ? 'dark' : 'light');
+ localStorage.setItem('simpleLedgerTheme', isDark ? 'dark' : 'light');
+ });
+ }
+
+ const openSidebar = () => {
+ if (!sidebar) return;
+ sidebar.classList.remove('-translate-x-full');
+ sidebar.classList.add('translate-x-0');
+ overlay?.classList.remove('hidden');
+ menuToggle?.setAttribute('aria-expanded', 'true');
+ };
+
+ const hideSidebar = () => {
+ if (!sidebar) return;
+ sidebar.classList.add('-translate-x-full');
+ sidebar.classList.remove('translate-x-0');
+ overlay?.classList.add('hidden');
+ menuToggle?.setAttribute('aria-expanded', 'false');
+ };
+
+ const maybeHideSidebar = () => {
+ if (window.innerWidth < 1024) {
+ hideSidebar();
+ }
+ };
+
+ menuToggle?.addEventListener('click', openSidebar);
+ closeSidebarBtn?.addEventListener('click', hideSidebar);
+ overlay?.addEventListener('click', hideSidebar);
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && window.innerWidth < 1024) {
+ hideSidebar();
+ }
+ });
+
+ window.addEventListener('resize', () => {
+ if (window.innerWidth >= 1024) {
+ sidebar?.classList.remove('-translate-x-full');
+ sidebar?.classList.remove('translate-x-0');
+ overlay?.classList.add('hidden');
+ menuToggle?.setAttribute('aria-expanded', 'false');
+ }
+ });
+
// Auth form toggling
document.getElementById('toggleAuth').onclick = (e) => {
e.preventDefault();
@@ -1071,74 +1223,114 @@
document.getElementById('registerForm').onsubmit = handleRegister;
// App Navigation
- document.querySelectorAll('nav a').forEach(a => {
- a.onclick = (e) => {
+ document.querySelectorAll('#sidebar nav a').forEach(link => {
+ link.onclick = (e) => {
e.preventDefault();
- if(e.target.id === 'logoutButton') return logout();
- showView(e.target.dataset.view);
- }
- });
-
- // --- Event Listeners ---
- document.getElementById('uploadForm').onsubmit = handleFiles;
- document.getElementById('categoryForm').onsubmit = addCategory;
- document.getElementById('vendorForm').onsubmit = addVendor;
- document.getElementById('ruleForm').onsubmit = addRule;
- document.getElementById('transactionFilter').onchange = renderTransactions;
- document.getElementById('transactionSearch').oninput = renderTransactions;
- document.getElementById('generateReportBtn').onclick = generateReport;
- document.getElementById('exportReportBtn').onclick = exportReport;
- document.getElementById('applyAllRulesBtn').onclick = applyAllRules;
-
- document.getElementById('dateFilterSelect').onchange = (e) => {
- const customPicker = document.getElementById('customDateRangePicker');
- if (e.target.value === 'custom') {
- customPicker.classList.remove('hidden');
- customPicker.classList.add('flex');
- } else {
- customPicker.classList.add('hidden');
- customPicker.classList.remove('flex');
- }
- };
-
- document.getElementById('filterByDateBtn').onclick = () => {
- const filterType = document.getElementById('dateFilterSelect').value;
- transactionDateFilter = filterType;
-
- if (filterType === 'custom') {
- const startDate = document.getElementById('startDate').value;
- const endDate = document.getElementById('endDate').value;
- if (startDate && endDate) {
- customDateRange = { start: new Date(startDate), end: new Date(endDate) };
- } else {
- swal('Error', 'Please select a start and end date for the custom range.', 'error');
- return;
+ const targetView = e.currentTarget.dataset.view;
+ if (targetView) {
+ showView(targetView);
+ maybeHideSidebar();
}
- }
- renderTransactions();
- };
+ };
+ });
- // Mobile menu
- const sidebar = document.getElementById('sidebar');
- const mobileMenuBtn = document.getElementById('mobileMenuBtn');
-
- if (mobileMenuBtn) {
- mobileMenuBtn.onclick = () => {
- sidebar.classList.toggle('-translate-x-full');
- sidebar.classList.toggle('hidden');
+ const logoutButton = document.getElementById('logoutButton');
+ if (logoutButton) {
+ logoutButton.onclick = (e) => {
+ e.preventDefault();
+ logout();
+ maybeHideSidebar();
};
}
- // Close sidebar when a nav link is clicked on mobile
- sidebar.querySelectorAll('a').forEach(link => {
- link.addEventListener('click', () => {
- if (window.innerWidth < 768) { // md breakpoint
- sidebar.classList.add('-translate-x-full');
- sidebar.classList.add('hidden');
- }
- });
- });
+ const quickTransactions = document.getElementById('quickTransactions');
+ if (quickTransactions) {
+ quickTransactions.onclick = () => {
+ showView('transactions');
+ maybeHideSidebar();
+ };
+ }
+ const quickUpload = document.getElementById('quickUpload');
+ if (quickUpload) {
+ quickUpload.onclick = () => {
+ showView('upload');
+ maybeHideSidebar();
+ };
+ }
+
+ // --- Event Listeners ---
+ const uploadForm = document.getElementById('uploadForm');
+ if (uploadForm) uploadForm.onsubmit = handleFiles;
+
+ const categoryForm = document.getElementById('categoryForm');
+ if (categoryForm) categoryForm.onsubmit = addCategory;
+
+ const vendorForm = document.getElementById('vendorForm');
+ if (vendorForm) vendorForm.onsubmit = addVendor;
+
+ const ruleForm = document.getElementById('ruleForm');
+ if (ruleForm) ruleForm.onsubmit = addRule;
+
+ const transactionFilterEl = document.getElementById('transactionFilter');
+ if (transactionFilterEl) transactionFilterEl.onchange = renderTransactions;
+
+ const transactionSearchEl = document.getElementById('transactionSearch');
+ if (transactionSearchEl) transactionSearchEl.oninput = renderTransactions;
+
+ const generateReportBtn = document.getElementById('generateReportBtn');
+ if (generateReportBtn) generateReportBtn.onclick = generateReport;
+
+ const exportReportBtn = document.getElementById('exportReportBtn');
+ if (exportReportBtn) exportReportBtn.onclick = exportReport;
+
+ const applyAllRulesBtn = document.getElementById('applyAllRulesBtn');
+ if (applyAllRulesBtn) applyAllRulesBtn.onclick = applyAllRules;
+
+ const dateFilterSelect = document.getElementById('dateFilterSelect');
+ if (dateFilterSelect) {
+ dateFilterSelect.onchange = (e) => {
+ const customPicker = document.getElementById('customDateRangePicker');
+ if (!customPicker) return;
+ transactionDateFilter = e.target.value;
+ if (e.target.value === 'custom') {
+ customPicker.classList.remove('hidden');
+ customPicker.classList.add('flex');
+ handleDateChange();
+ } else {
+ customPicker.classList.add('hidden');
+ customPicker.classList.remove('flex');
+ customDateRange = { start: null, end: null };
+ renderTransactions();
+ }
+ };
+ }
+ renderTransactions();
+
+ const handleDateChange = () => {
+ if (transactionDateFilter !== 'custom') return;
+ const startDateVal = document.getElementById('startDate')?.value;
+ const endDateVal = document.getElementById('endDate')?.value;
+ if (startDateVal && endDateVal) {
+ customDateRange = { start: new Date(startDateVal), end: new Date(endDateVal) };
+ renderTransactions();
+ }
+ };
+
+ const startDateInput = document.getElementById('startDate');
+ const endDateInput = document.getElementById('endDate');
+ if (startDateInput) startDateInput.onchange = handleDateChange;
+ if (endDateInput) endDateInput.onchange = handleDateChange;
+
+ const mobileSortSelectEl = document.getElementById('mobileSortSelect');
+ if (mobileSortSelectEl) {
+ mobileSortSelectEl.onchange = (e) => {
+ const [column, direction] = e.target.value.split('-');
+ transactionSort.column = column;
+ transactionSort.direction = direction;
+ renderTransactions();
+ };
+ }
// Transaction table sorting
document.querySelectorAll('.sortable-header').forEach(th => {
th.onclick = handleSort;
diff --git a/index.html b/index.html
index b93d5eb..f7103dc 100644
--- a/index.html
+++ b/index.html
@@ -4,6 +4,10 @@
SimpleLedger - Your Personal Bookkeeper
+
+
+
+
@@ -15,7 +19,7 @@
}
.spinner {
border: 4px solid rgba(0, 0, 0, .1);
- border-left-color: #2563eb;
+ border-left-color: #6366f1;
border-radius: 50%;
width: 40px;
height: 40px;
@@ -34,7 +38,12 @@
user-select: none;
}
.sortable-header:hover {
- background-color: #f9fafb;
+ background-color: rgba(148, 163, 184, 0.16);
+ color: #2563eb;
+ }
+ body.dark-mode .sortable-header:hover {
+ background-color: rgba(30, 41, 59, 0.8);
+ color: #93c5fd;
}
.sortable-header.sort-asc::after {
content: ' ▲';
@@ -46,87 +55,91 @@
}
-
+
-
-
-
Login
-
+
+
+
+
SimpleLedger
+
Login
+
Track income, tame expenses, celebrate wins.
+
+
-