update mobile experiance

This commit is contained in:
chris 2025-11-06 00:32:05 -05:00
parent bfb1ca62ce
commit 5985375755
2 changed files with 696 additions and 318 deletions

404
app.js
View File

@ -184,7 +184,7 @@
list.innerHTML = ''; list.innerHTML = '';
categories.sort((a,b) => a.name.localeCompare(b.name)).forEach(cat => { categories.sort((a,b) => a.name.localeCompare(b.name)).forEach(cat => {
const el = document.createElement('div'); 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 = ` el.innerHTML = `
<span>${cat.name} (${cat.type})</span> <span>${cat.name} (${cat.type})</span>
<button data-id="${cat.id}" class="text-red-500 hover:text-red-700">&times;</button> <button data-id="${cat.id}" class="text-red-500 hover:text-red-700">&times;</button>
@ -213,7 +213,7 @@
list.innerHTML = ''; list.innerHTML = '';
vendors.forEach(v => { vendors.forEach(v => {
const el = document.createElement('div'); 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 = ` el.innerHTML = `
<span>${v.name}</span> <span>${v.name}</span>
<button data-id="${v.id}" class="text-red-500 hover:text-red-700">&times;</button> <button data-id="${v.id}" class="text-red-500 hover:text-red-700">&times;</button>
@ -242,7 +242,7 @@
list.innerHTML = ''; list.innerHTML = '';
rules.forEach(rule => { rules.forEach(rule => {
const el = document.createElement('div'); 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 = ` el.innerHTML = `
<span>If description contains "<strong>${rule.keyword}</strong>", set category to <strong>${rule.category}</strong></span> <span>If description contains "<strong>${rule.keyword}</strong>", set category to <strong>${rule.category}</strong></span>
<button data-id="${rule.id}" class="text-red-500 hover:text-red-700">&times;</button> <button data-id="${rule.id}" class="text-red-500 hover:text-red-700">&times;</button>
@ -254,9 +254,15 @@
function renderTransactions() { function renderTransactions() {
const tbody = document.getElementById('transactionsTableBody'); const tbody = document.getElementById('transactionsTableBody');
const filter = document.getElementById('transactionFilter').value; const mobileList = document.getElementById('transactionsListMobile');
const search = document.getElementById('transactionSearch').value.toLowerCase(); const filterEl = document.getElementById('transactionFilter');
if (!tbody) return; 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]; let txsToRender = [...transactions];
@ -296,75 +302,146 @@
return 0; return 0;
}); });
// 2. Filter by Category Status // 3. Filter by Category Status
if (filter === 'uncategorized') { if (filter === 'uncategorized') {
txsToRender = txsToRender.filter(tx => tx.reconciliation_status === 'uncategorized'); txsToRender = txsToRender.filter(tx => tx.reconciliation_status === 'uncategorized');
} }
// 3. Filter by Search // 4. Filter by Search
if (search) { if (search) {
txsToRender = txsToRender.filter(tx => tx.description.toLowerCase().includes(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 => { document.querySelectorAll('.sortable-header').forEach(th => {
th.classList.remove('sort-asc', 'sort-desc'); th.classList.remove('sort-asc', 'sort-desc');
if (th.dataset.sort === transactionSort.column) { if (th.dataset.sort === transactionSort.column) {
th.classList.add(transactionSort.direction === 'asc' ? 'sort-asc' : 'sort-desc'); 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;
}
}
// 6. Render
if (tbody) tbody.innerHTML = '';
if (mobileList) mobileList.innerHTML = '';
// 5. Render
tbody.innerHTML = '';
if (txsToRender.length === 0) { if (txsToRender.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center p-4">No transactions found.</td></tr>'; if (tbody) {
tbody.innerHTML = '<tr><td colspan="6" class="p-6 text-center text-slate-500 dark:text-slate-300">No transactions found.</td></tr>';
}
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; return;
} }
txsToRender.forEach(tx => { txsToRender.forEach(tx => {
const txDate = new Date(tx.date); 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 categoryDisplay = '';
let categoryLabel = '';
let vendorDisplay = ''; let vendorDisplay = '';
let vendorLabel = '';
if (tx.reconciliation_status === 'uncategorized') { if (tx.reconciliation_status === 'uncategorized') {
categoryDisplay = '<span class="text-red-500">Uncategorized</span>'; categoryDisplay = '<span class="text-xs font-semibold uppercase tracking-wide text-rose-500">Uncategorized</span>';
categoryLabel = 'Uncategorized';
vendorDisplay = 'N/A'; vendorDisplay = 'N/A';
vendorLabel = 'N/A';
} else if (tx.split_count === 1) { } else if (tx.split_count === 1) {
categoryDisplay = tx.category || 'N/A'; categoryDisplay = tx.category || 'N/A';
categoryLabel = tx.category || 'N/A';
vendorDisplay = tx.vendorName || 'N/A'; vendorDisplay = tx.vendorName || 'N/A';
vendorLabel = tx.vendorName || 'N/A';
} else if (tx.split_count > 1) { } else if (tx.split_count > 1) {
categoryDisplay = '— Split —'; categoryDisplay = '— Split —';
categoryLabel = 'Split';
vendorDisplay = '— Split —'; vendorDisplay = '— Split —';
} else { // split_count is 0 or null vendorLabel = 'Split';
categoryDisplay = '<span class="text-orange-500">Needs Attention</span>'; } else {
categoryDisplay = '<span class="text-xs font-semibold uppercase tracking-wide text-orange-500">Needs Attention</span>';
categoryLabel = 'Needs attention';
vendorDisplay = 'N/A'; vendorDisplay = 'N/A';
vendorLabel = 'N/A';
} }
tr.innerHTML = ` if (tbody) {
<td class="p-3">${txDate.toLocaleDateString()}</td> const tr = document.createElement('tr');
<td class="p-3 truncate max-w-xs">${tx.description}</td> tr.className = 'border-b border-slate-100 transition dark:border-slate-800 hover:bg-slate-50/80 dark:hover:bg-slate-800/60';
<td class="p-3">${categoryDisplay}</td> tr.innerHTML = `
<td class="p-3">${vendorDisplay}</td> <td class="p-3">${txDate.toLocaleDateString()}</td>
<td class="p-3 text-right font-medium ${tx.amount >= 0 ? 'text-green-600' : 'text-red-600'}"> <td class="p-3 truncate max-w-xs">${tx.description}</td>
${tx.amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' })} <td class="p-3">${categoryDisplay}</td>
</td> <td class="p-3">${vendorDisplay}</td>
<td class="p-3 text-center"> <td class="p-3 text-right font-medium ${tx.amount >= 0 ? 'text-green-600' : 'text-red-600'}">
<button data-id="${tx.id}" class="edit-tx-btn text-blue-600 hover:underline text-sm">Edit</button> ${tx.amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' })}
<button data-id="${tx.id}" class="delete-tx-btn text-red-600 hover:underline text-sm ml-2">Del</button> </td>
</td> <td class="p-3 text-center">
`; <button data-id="${tx.id}" class="edit-tx-btn text-blue-600 hover:underline text-sm">Edit</button>
tbody.appendChild(tr); <button data-id="${tx.id}" class="delete-tx-btn text-red-600 hover:underline text-sm ml-2">Del</button>
</td>
`;
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 = `
<div class="flex items-start justify-between gap-4">
<div class="space-y-1">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-600 dark:text-slate-400">${txDate.toLocaleDateString()}</p>
<p class="text-base font-semibold text-slate-900 dark:text-white">${tx.description}</p>
</div>
<div class="text-right">
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-500">${tx.amount >= 0 ? 'Incoming' : 'Outgoing'}</p>
<p class="text-lg font-bold ${amountColor}">${tx.amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' })}</p>
</div>
</div>
<div class="flex flex-wrap gap-2 text-xs font-semibold text-slate-600 dark:text-slate-300">
<span class="rounded-full bg-slate-100 px-3 py-1 text-slate-700 dark:bg-slate-800/70 dark:text-slate-200">${categoryLabel}</span>
<span class="rounded-full bg-slate-100 px-3 py-1 text-slate-700 dark:bg-slate-800/70 dark:text-slate-200">Vendor: ${vendorLabel}</span>
<span class="rounded-full bg-blue-100 px-3 py-1 text-blue-600 dark:bg-blue-500/20 dark:text-blue-200">${tx.reconciliation_status}</span>
</div>
<div class="flex flex-wrap gap-3 pt-2">
<button data-id="${tx.id}" class="mobile-edit-tx-btn btn bg-white/85 px-4 py-2 text-sm font-semibold text-slate-700 hover:-translate-y-0.5 hover:shadow-md dark:bg-slate-800/70 dark:text-slate-200">
Edit
</button>
<button data-id="${tx.id}" class="mobile-delete-tx-btn btn bg-rose-500/90 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-rose-500/30 hover:-translate-y-0.5 hover:shadow-xl">
Delete
</button>
</div>
`;
mobileList.appendChild(card);
}
}); });
tbody.querySelectorAll('.edit-tx-btn').forEach(btn => { if (tbody) {
btn.onclick = (e) => showSplitTransactionModal(e.target.dataset.id); 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); 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) { function handleSort(e) {
@ -1026,18 +1103,20 @@
document.querySelectorAll('main > section').forEach(section => { document.querySelectorAll('main > section').forEach(section => {
section.classList.add('hidden'); section.classList.add('hidden');
}); });
document.querySelectorAll('nav a').forEach(a => { document.querySelectorAll('#sidebar nav a').forEach(a => {
a.classList.remove('bg-blue-700', 'text-white'); a.classList.remove('active-nav');
a.classList.add('text-blue-100', 'hover:bg-blue-600'); a.removeAttribute('aria-current');
}); });
const activeView = document.getElementById(viewId); 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) { if (activeLink) {
activeLink.classList.add('bg-blue-700', 'text-white'); activeLink.classList.add('active-nav');
activeLink.classList.remove('text-blue-100', 'hover:bg-blue-600'); activeLink.setAttribute('aria-current', 'page');
} }
// Refresh dashboard data only when switching to it // Refresh dashboard data only when switching to it
@ -1059,6 +1138,79 @@
// --- Initial App Load --- // --- Initial App Load ---
document.addEventListener('DOMContentLoaded', () => { 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 // Auth form toggling
document.getElementById('toggleAuth').onclick = (e) => { document.getElementById('toggleAuth').onclick = (e) => {
e.preventDefault(); e.preventDefault();
@ -1071,74 +1223,114 @@
document.getElementById('registerForm').onsubmit = handleRegister; document.getElementById('registerForm').onsubmit = handleRegister;
// App Navigation // App Navigation
document.querySelectorAll('nav a').forEach(a => { document.querySelectorAll('#sidebar nav a').forEach(link => {
a.onclick = (e) => { link.onclick = (e) => {
e.preventDefault(); e.preventDefault();
if(e.target.id === 'logoutButton') return logout(); const targetView = e.currentTarget.dataset.view;
showView(e.target.dataset.view); if (targetView) {
} showView(targetView);
maybeHideSidebar();
}
};
}); });
// --- Event Listeners --- const logoutButton = document.getElementById('logoutButton');
document.getElementById('uploadForm').onsubmit = handleFiles; if (logoutButton) {
document.getElementById('categoryForm').onsubmit = addCategory; logoutButton.onclick = (e) => {
document.getElementById('vendorForm').onsubmit = addVendor; e.preventDefault();
document.getElementById('ruleForm').onsubmit = addRule; logout();
document.getElementById('transactionFilter').onchange = renderTransactions; maybeHideSidebar();
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;
}
}
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');
}; };
} }
// Close sidebar when a nav link is clicked on mobile const quickTransactions = document.getElementById('quickTransactions');
sidebar.querySelectorAll('a').forEach(link => { if (quickTransactions) {
link.addEventListener('click', () => { quickTransactions.onclick = () => {
if (window.innerWidth < 768) { // md breakpoint showView('transactions');
sidebar.classList.add('-translate-x-full'); maybeHideSidebar();
sidebar.classList.add('hidden'); };
} }
});
});
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 // Transaction table sorting
document.querySelectorAll('.sortable-header').forEach(th => { document.querySelectorAll('.sortable-header').forEach(th => {
th.onclick = handleSort; th.onclick = handleSort;

View File

@ -4,6 +4,10 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SimpleLedger - Your Personal Bookkeeper</title> <title>SimpleLedger - Your Personal Bookkeeper</title>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.min.js"></script>
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script> <script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
@ -15,7 +19,7 @@
} }
.spinner { .spinner {
border: 4px solid rgba(0, 0, 0, .1); border: 4px solid rgba(0, 0, 0, .1);
border-left-color: #2563eb; border-left-color: #6366f1;
border-radius: 50%; border-radius: 50%;
width: 40px; width: 40px;
height: 40px; height: 40px;
@ -34,7 +38,12 @@
user-select: none; user-select: none;
} }
.sortable-header:hover { .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 { .sortable-header.sort-asc::after {
content: ' ▲'; content: ' ▲';
@ -46,87 +55,91 @@
} }
</style> </style>
</head> </head>
<body class="bg-gray-100"> <body class="bg-slate-100 text-slate-900 dark:bg-gray-950 dark:text-slate-100">
<div id="authView" class="flex items-center justify-center min-h-screen"> <div id="authView" class="flex min-h-screen items-center justify-center bg-gradient-to-br from-slate-900 via-blue-900 to-indigo-900 px-4 py-16">
<div class="bg-white p-8 rounded-lg shadow-md w-full max-w-sm"> <div class="card w-full max-w-md bg-white/90 backdrop-blur-md dark:bg-gray-900/90 dark:ring-1 dark:ring-white/10">
<h2 id="authTitle" class="text-2xl font-bold mb-6 text-center">Login</h2> <div class="mb-6 text-center">
<div id="authError" class="text-red-500 text-sm mb-4 hidden"></div> <span class="inline-flex items-center rounded-full bg-blue-100 px-3 py-1 text-xs font-semibold uppercase tracking-[0.3em] text-blue-600 dark:bg-blue-500/20 dark:text-blue-300">SimpleLedger</span>
<h2 id="authTitle" class="mt-4 text-3xl font-semibold text-gray-900 dark:text-white">Login</h2>
<p class="mt-2 text-sm text-slate-500 dark:text-slate-400">Track income, tame expenses, celebrate wins.</p>
</div>
<div id="authError" class="mb-4 hidden rounded-lg border border-red-100 bg-red-50 px-4 py-3 text-sm text-red-600 dark:border-red-500/40 dark:bg-red-500/10"></div>
<form id="loginForm"> <form id="loginForm" class="space-y-4">
<div class="mb-4"> <div>
<label for="loginUsername" class="block text-sm font-medium text-gray-700">Username</label> <label for="loginUsername" class="block text-sm font-medium text-slate-600 dark:text-slate-300">Username</label>
<input type="text" id="loginUsername" name="username" required class="mt-1 w-full p-2 border rounded-md shadow-sm"> <input type="text" id="loginUsername" name="username" required class="form-control mt-1 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div> </div>
<div class="mb-6"> <div>
<label for="loginPassword" class="block text-sm font-medium text-gray-700">Password</label> <label for="loginPassword" class="block text-sm font-medium text-slate-600 dark:text-slate-300">Password</label>
<input type="password" id="loginPassword" name="password" required class="mt-1 w-full p-2 border rounded-md shadow-sm"> <input type="password" id="loginPassword" name="password" required class="form-control mt-1 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div> </div>
<button type="submit" class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg font-semibold hover:bg-blue-700"> <button type="submit" class="btn btn-primary w-full bg-gradient-to-r from-blue-600 via-indigo-500 to-purple-500 text-white shadow-lg shadow-indigo-500/30 transition hover:-translate-y-0.5 hover:shadow-xl">
Login Login
</button> </button>
</form> </form>
<form id="registerForm" class="hidden"> <form id="registerForm" class="hidden space-y-4">
<div class="mb-4"> <div>
<label for="regUsername" class="block text-sm font-medium text-gray-700">Username</label> <label for="regUsername" class="block text-sm font-medium text-slate-600 dark:text-slate-300">Username</label>
<input type="text" id="regUsername" name="username" required class="mt-1 w-full p-2 border rounded-md shadow-sm"> <input type="text" id="regUsername" name="username" required class="form-control mt-1 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div> </div>
<div class="mb-6"> <div>
<label for="regPassword" class="block text-sm font-medium text-gray-700">Password</label> <label for="regPassword" class="block text-sm font-medium text-slate-600 dark:text-slate-300">Password</label>
<input type="password" id="regPassword" name="password" required class="mt-1 w-full p-2 border rounded-md shadow-sm"> <input type="password" id="regPassword" name="password" required class="form-control mt-1 focus:outline-none focus:ring-2 focus:ring-blue-500">
</div> </div>
<button type="submit" class="w-full bg-green-600 text-white py-2 px-4 rounded-lg font-semibold hover:bg-green-700"> <button type="submit" class="btn btn-primary w-full bg-gradient-to-r from-blue-600 via-indigo-500 to-purple-500 text-white shadow-lg shadow-indigo-500/30 transition hover:-translate-y-0.5 hover:shadow-xl">
Register Register
</button> </button>
</form> </form>
<p class="text-center text-sm text-gray-600 mt-6"> <p class="mt-6 text-center text-sm text-slate-500 dark:text-slate-400">
<a href="#" id="toggleAuth" class="text-blue-600 hover:underline">Need an account? Register</a> <a href="#" id="toggleAuth" class="font-semibold text-blue-500 hover:underline">Need an account? Register</a>
</p> </p>
</div> </div>
</div> </div>
<div id="loadingModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div id="loadingModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-slate-900/60 backdrop-blur">
<div class="bg-white p-6 rounded-lg shadow-xl flex items-center"> <div class="card flex items-center gap-4 bg-white/90 text-slate-700 shadow-2xl dark:bg-slate-900/90 dark:text-slate-100">
<div class="spinner mr-4"></div> <div class="spinner mr-1"></div>
<span id="loadingModalText" class="text-lg">Loading...</span> <span id="loadingModalText" class="text-lg font-semibold">Loading...</span>
</div> </div>
</div> </div>
<div id="editTxModal" class="modal hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-40"> <div id="editTxModal" class="modal hidden fixed inset-0 z-40 flex items-center justify-center bg-slate-900/60 backdrop-blur">
<div class="modal-content bg-white p-6 rounded-lg shadow-xl w-full max-w-3xl"> <div class="modal-content card w-full max-w-md mx-4 space-y-4">
<input type="hidden" id="splitTxId"> <input type="hidden" id="splitTxId">
<input type="hidden" id="splitTxTotal"> <input type="hidden" id="splitTxTotal">
<div class="flex justify-between items-center mb-4"> <div class="flex items-center justify-between">
<div> <div>
<h3 class="text-xl font-semibold" id="splitModalTitle">Split Transaction</h3> <h3 class="text-xl font-semibold text-slate-900 dark:text-white" id="splitModalTitle">Split Transaction</h3>
<p class="text-sm text-gray-600" id="splitModalDescription"></p> <p class="text-sm text-slate-500 dark:text-slate-300" id="splitModalDescription"></p>
</div> </div>
<div class="text-right"> <div class="text-right">
<div class="text-lg font-bold" id="splitModalTotal"></div> <div class="text-lg font-bold text-slate-900 dark:text-white" id="splitModalTotal"></div>
<div class="text-sm font-medium" id="splitModalRemaining"></div> <div class="text-sm font-medium text-slate-500 dark:text-slate-300" id="splitModalRemaining"></div>
</div> </div>
</div> </div>
<div id="splitsContainer" class="max-h-96 overflow-y-auto space-y-3 p-2 bg-gray-50 rounded"> <div id="splitsContainer" class="max-h-96 space-y-3 overflow-y-auto rounded-xl bg-slate-50/70 p-3 dark:bg-slate-800/60">
</div> </div>
<div class="flex justify-between items-center mt-6"> <div class="flex items-center justify-between">
<div> <div>
<button type="button" id="addSplitBtn" class="bg-green-600 text-white py-2 px-4 rounded-lg font-semibold hover:bg-green-700"> <button type="button" id="addSplitBtn" class="btn btn-primary bg-gradient-to-r from-emerald-500 to-teal-500 px-4 py-2 text-sm text-white shadow-lg shadow-emerald-500/30 hover:-translate-y-0.5 hover:shadow-xl">
Add Split Add Split
</button> </button>
<button type="button" id="createRuleFromSplit" class="bg-indigo-600 text-white py-2 px-4 rounded-lg font-semibold hover:bg-indigo-700"> <button type="button" id="createRuleFromSplit" class="btn btn-primary bg-gradient-to-r from-indigo-500 to-purple-500 px-4 py-2 text-sm text-white shadow-lg shadow-indigo-500/30 hover:-translate-y-0.5 hover:shadow-xl">
Create Rule Create Rule
</button> </button>
</div> </div>
<div class="flex space-x-3"> <div class="flex space-x-3">
<button type="button" id="closeTxModalBtn" class="bg-gray-300 text-gray-800 py-2 px-4 rounded-lg font-semibold hover:bg-gray-400"> <button type="button" id="closeTxModalBtn" class="btn bg-white/80 px-4 py-2 text-sm font-semibold text-slate-600 hover:-translate-y-0.5 hover:shadow-md dark:bg-slate-800/70 dark:text-slate-200">
Cancel Cancel
</button> </button>
<button type="button" id="saveSplitsBtn" class="bg-blue-600 text-white py-2 px-4 rounded-lg font-semibold hover:bg-blue-700 disabled:opacity-50"> <button type="button" id="saveSplitsBtn" class="btn btn-primary bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500 px-4 py-2 text-sm text-white shadow-lg shadow-indigo-500/30 hover:-translate-y-0.5 hover:shadow-xl disabled:cursor-not-allowed disabled:opacity-50">
Save Changes Save Changes
</button> </button>
</div> </div>
@ -135,258 +148,431 @@
</div> </div>
<div id="appContainer" class="hidden relative min-h-screen md:flex"> <div id="appContainer" class="hidden min-h-screen bg-slate-100 dark:bg-gray-950">
<!-- Mobile menu button --> <div class="relative flex min-h-screen w-full overflow-hidden">
<div class="md:hidden flex justify-between items-center bg-blue-800 text-white p-4"> <div id="mobileOverlay" class="fixed inset-0 z-30 hidden bg-slate-900/50 backdrop-blur-sm lg:hidden"></div>
<h1 class="text-2xl font-bold">SimpleLedger</h1>
<button id="mobileMenuBtn">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7"></path></svg>
</button>
</div>
<nav id="sidebar" class="hidden md:flex w-64 bg-blue-800 text-white flex-col p-4 absolute inset-y-0 left-0 z-30 md:relative md:translate-x-0 transform -translate-x-full transition-transform duration-200 ease-in-out"> <aside id="sidebar" class="fixed inset-y-0 left-0 z-40 flex w-72 -translate-x-full transform flex-col gap-6 overflow-y-auto border-r border-white/40 bg-white/95 px-4 py-8 shadow-2xl transition-transform duration-300 ease-out dark:border-gray-800 dark:bg-gray-900/95 lg:static lg:translate-x-0 lg:shadow-none">
<h1 class="text-2xl font-bold mb-6">SimpleLedger</h1> <div class="flex items-start justify-between gap-4 px-1">
<a href="#" data-view="dashboard" class="block p-3 rounded-lg text-blue-100 hover:bg-blue-600">Dashboard</a>
<a href="#" data-view="transactions" class="block p-3 rounded-lg text-blue-100 hover:bg-blue-600">Transactions</a>
<a href="#" data-view="upload" class="block p-3 rounded-lg text-blue-100 hover:bg-blue-600">Upload</a>
<a href="#" data-view="categories" class="block p-3 rounded-lg text-blue-100 hover:bg-blue-600">Categories</a>
<a href="#" data-view="vendors" class="block p-3 rounded-lg text-blue-100 hover:bg-blue-600">Vendors</a>
<a href="#" data-view="rules" class="block p-3 rounded-lg text-blue-100 hover:bg-blue-600">Rules</a>
<a href="#" data-view="reports" class="block p-3 rounded-lg text-blue-100 hover:bg-blue-600">Reports</a>
<div class="mt-auto">
<a href="#" id="logoutButton" class="p-3 rounded-lg text-blue-100 hover:bg-blue-600 block">Logout</a>
</div>
</nav>
<main class="flex-1 p-4 md:p-8 overflow-y-auto">
<section id="dashboard">
<h2 class="text-3xl font-bold mb-6">Dashboard</h2>
<div class="grid grid-cols-2 md:grid-cols-5 gap-6 mb-6">
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-sm font-medium text-gray-500">Gross Income</h3>
<p id="statGrossIncome" class="text-2xl font-semibold text-green-600">$0.00</p>
</div>
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-sm font-medium text-gray-500">Returns & Discounts</h3>
<p id="statReturns" class="text-2xl font-semibold text-yellow-600">$0.00</p>
</div>
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-sm font-medium text-gray-500">Net Income</h3>
<p id="statNetIncome" class="text-2xl font-semibold text-green-700">$0.00</p>
</div>
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-sm font-medium text-gray-500">Total Expenses</h3>
<p id="statTotalExpenses" class="text-2xl font-semibold text-red-600">$0.00</p>
</div>
<div class="bg-white p-6 rounded-lg shadow-md">
<h3 class="text-sm font-medium text-gray-500">Net Profit / (Loss)</h3>
<p id="statNetProfit" class="text-2xl font-semibold">$0.00</p>
</div>
</div>
<div class="bg-white p-6 rounded-lg shadow-md h-96">
<h3 class="text-xl font-semibold mb-4">Top 10 Expenses</h3>
<div class="relative h-full w-full max-h-[300px]">
<canvas id="expensePieChart"></canvas>
</div>
</div>
</section>
<section id="transactions" class="hidden">
<div class="flex justify-between items-center mb-6">
<h2 class="text-3xl font-bold">Transactions</h2>
<div class="flex items-end space-x-4">
<div> <div>
<label for="dateFilterSelect" class="text-sm font-medium text-gray-700">Date Range</label> <span class="text-xs font-semibold uppercase tracking-[0.4em] text-blue-500">SimpleLedger</span>
<select id="dateFilterSelect" class="p-2 border rounded-md shadow-sm"> <h1 class="mt-3 text-2xl font-bold text-slate-900 dark:text-white">Money Magic</h1>
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">Your playful finance HQ</p>
</div>
<button id="closeSidebar" class="inline-flex h-10 w-10 items-center justify-center rounded-full bg-slate-100 text-slate-500 shadow-sm transition hover:-translate-y-0.5 hover:bg-slate-200 hover:text-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700 lg:hidden" aria-label="Close navigation">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<nav class="flex-1 space-y-2">
<a href="#" data-view="dashboard" class="nav-link">
<span class="nav-icon bg-blue-500/10 text-blue-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 9.75L12 4l9 5.75M21 9.75V19a2 2 0 01-2 2h-2.25a.75.75 0 01-.75-.75V14a2 2 0 00-2-2h-3a2 2 0 00-2 2v6.25a.75.75 0 01-.75.75H6a2 2 0 01-2-2V9.75" />
</svg>
</span>
<span>Dashboard</span>
</a>
<a href="#" data-view="transactions" class="nav-link">
<span class="nav-icon bg-indigo-500/10 text-indigo-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.75 7.5h16.5M3.75 12h16.5M3.75 16.5h10.5" />
</svg>
</span>
<span>Transactions</span>
</a>
<a href="#" data-view="upload" class="nav-link">
<span class="nav-icon bg-emerald-500/10 text-emerald-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 4v12m0 0l-4-4m4 4 4-4M6 20h12" />
</svg>
</span>
<span>Upload</span>
</a>
<a href="#" data-view="categories" class="nav-link">
<span class="nav-icon bg-amber-500/10 text-amber-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.75 5.25h16.5M5.25 5.25V18a2.25 2.25 0 002.25 2.25h9a2.25 2.25 0 002.25-2.25V5.25" />
</svg>
</span>
<span>Categories</span>
</a>
<a href="#" data-view="vendors" class="nav-link">
<span class="nav-icon bg-fuchsia-500/10 text-fuchsia-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4.5 9.75l7.5-6 7.5 6M19.5 9.75V18a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 18V9.75" />
</svg>
</span>
<span>Vendors</span>
</a>
<a href="#" data-view="rules" class="nav-link">
<span class="nav-icon bg-purple-500/10 text-purple-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8.25 4.5h10.5m-10.5 0A2.25 2.25 0 006 6.75v10.5a2.25 2.25 0 002.25 2.25h10.5A2.25 2.25 0 0021 17.25V6.75A2.25 2.25 0 0018.75 4.5m-10.5 0V3.75A1.125 1.125 0 019.375 2.625h7.875A1.125 1.125 0 0118.375 3.75V4.5m-10.5 0h10.5" />
</svg>
</span>
<span>Rules</span>
</a>
<a href="#" data-view="reports" class="nav-link">
<span class="nav-icon bg-rose-500/10 text-rose-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 12h3l2-5 4 10 3-6h6" />
</svg>
</span>
<span>Reports</span>
</a>
</nav>
<div class="space-y-3 border-t border-slate-200 pt-4 dark:border-slate-800">
<button id="darkModeToggle" class="flex w-full items-center justify-between rounded-xl bg-slate-900 px-4 py-3 text-sm font-semibold text-white shadow-lg shadow-slate-900/30 transition hover:-translate-y-0.5 hover:shadow-xl dark:bg-slate-200 dark:text-slate-900 dark:shadow-slate-200/40">
<span>Toggle dark mode</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 3v1.5m0 15V21m9-9h-1.5M4.5 12H3m15.364 6.364l-1.06-1.06M7.696 7.696 6.636 6.636m11.314 0-1.06 1.06M7.696 16.304l-1.06 1.06M16.5 12a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0z" />
</svg>
</button>
<a href="#" id="logoutButton" class="flex items-center justify-between rounded-xl bg-white/90 px-4 py-3 text-sm font-semibold text-slate-600 shadow-inner transition hover:-translate-y-0.5 hover:bg-white dark:bg-slate-800/80 dark:text-slate-200 dark:hover:bg-slate-800">
<span>Logout</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6A2.25 2.25 0 005.25 5.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9" />
</svg>
</a>
</div>
</aside>
<div class="flex flex-1 flex-col lg:ml-72">
<header class="sticky top-0 z-20 flex items-center justify-between gap-4 border-b border-slate-200/60 bg-slate-100/80 px-4 py-4 backdrop-blur-md dark:border-slate-800/60 dark:bg-gray-950/80 sm:px-6 lg:px-10">
<div class="flex items-center gap-3">
<button id="menuToggle" class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-white text-slate-600 shadow-lg shadow-slate-400/40 transition hover:-translate-y-0.5 hover:text-slate-900 dark:bg-slate-800 dark:text-slate-300 dark:shadow-none lg:hidden" aria-label="Open navigation">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 6h16M4 12h16M4 18h10" />
</svg>
</button>
<div>
<p class="text-xs font-semibold uppercase tracking-[0.3em] text-slate-500 dark:text-slate-400">Welcome back</p>
<h2 class="mt-1 text-2xl font-bold text-slate-900 dark:text-white">Let's balance the books</h2>
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<button id="quickTransactions" class="rounded-full border border-transparent bg-white px-4 py-2 text-sm font-semibold text-slate-700 shadow-sm transition hover:-translate-y-0.5 hover:bg-slate-100 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700">Transactions</button>
<button id="quickUpload" class="rounded-full bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-indigo-500/30 transition hover:-translate-y-0.5 hover:shadow-xl">Upload CSVs</button>
</div>
</header>
<main class="flex-1 overflow-y-auto px-4 pb-16 pt-8 sm:px-6 lg:px-10">
<section id="dashboard" class="space-y-6">
<div>
<h2 class="text-3xl font-semibold text-slate-900 dark:text-white">Dashboard</h2>
<p class="mt-2 text-sm text-slate-500 dark:text-slate-400">Get a pulse on your business at a glance.</p>
</div>
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
<div class="card stat-card">
<div class="stat-icon bg-emerald-500/15 text-emerald-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l5-5 3 3 8-8" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 20h16" />
</svg>
</div>
<div>
<h3 class="stat-label">Gross Income</h3>
<p id="statGrossIncome" class="stat-value text-emerald-500">$0.00</p>
</div>
</div>
<div class="card stat-card">
<div class="stat-icon bg-amber-500/15 text-amber-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 7h8a3 3 0 110 6H8" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.5 10.5L8 13l2.5 2.5" />
</svg>
</div>
<div>
<h3 class="stat-label">Returns &amp; Discounts</h3>
<p id="statReturns" class="stat-value text-amber-500">$0.00</p>
</div>
</div>
<div class="card stat-card">
<div class="stat-icon bg-green-500/15 text-green-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 19V5" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 9l4-4 4 4" />
</svg>
</div>
<div>
<h3 class="stat-label">Net Income</h3>
<p id="statNetIncome" class="stat-value text-green-500">$0.00</p>
</div>
</div>
<div class="card stat-card">
<div class="stat-icon bg-rose-500/15 text-rose-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 5v14" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16 15l-4 4-4-4" />
</svg>
</div>
<div>
<h3 class="stat-label">Total Expenses</h3>
<p id="statTotalExpenses" class="stat-value text-rose-500">$0.00</p>
</div>
</div>
<div class="card stat-card">
<div class="stat-icon bg-indigo-500/15 text-indigo-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 5v14" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 9h12" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7.5 9L5 13h5L7.5 9z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16.5 9L14 13h5l-2.5-4z" />
</svg>
</div>
<div>
<h3 class="stat-label">Net Profit / (Loss)</h3>
<p id="statNetProfit" class="stat-value">$0.00</p>
</div>
</div>
</div>
<div class="card h-96 space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-xl font-semibold text-slate-900 dark:text-slate-100">Top 10 Expenses</h3>
<span class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-slate-500 dark:bg-slate-800/70 dark:text-slate-300">Live view</span>
</div>
<div class="relative h-full w-full max-h-[320px]">
<canvas id="expensePieChart"></canvas>
</div>
</div>
</section>
<section id="transactions" class="hidden space-y-6">
<div class="card space-y-4">
<div class="flex flex-wrap items-center justify-between gap-4">
<h2 class="text-2xl font-semibold text-slate-900 dark:text-white">Transactions</h2>
<p class="text-sm text-slate-500 dark:text-slate-400">Filter, search, and split with ease.</p>
</div>
<div class="flex flex-col gap-4 md:flex-row md:flex-wrap md:items-end">
<div class="w-full min-w-[180px] md:w-auto">
<label for="dateFilterSelect" class="text-sm font-medium text-slate-600 dark:text-slate-300">Date Range</label>
<select id="dateFilterSelect" class="form-control mt-1">
<option value="all">All Time</option> <option value="all">All Time</option>
<option value="lastMonth" selected>Last Month</option> <option value="lastMonth" selected>Last Month</option>
<option value="custom">Custom</option> <option value="custom">Custom</option>
</select> </select>
</div> </div>
<div id="customDateRangePicker" class="hidden items-end space-x-4"> <div id="customDateRangePicker" class="hidden w-full flex-wrap items-end gap-4 md:w-auto">
<div> <div class="w-full min-w-[160px] md:w-auto">
<label for="startDate" class="text-sm font-medium text-gray-700">Start Date</label> <label for="startDate" class="text-sm font-medium text-slate-600 dark:text-slate-300">Start Date</label>
<input type="date" id="startDate" class="p-2 border rounded-md shadow-sm"> <input type="date" id="startDate" class="form-control mt-1">
</div> </div>
<div> <div class="w-full min-w-[160px] md:w-auto">
<label for="endDate" class="text-sm font-medium text-gray-700">End Date</label> <label for="endDate" class="text-sm font-medium text-slate-600 dark:text-slate-300">End Date</label>
<input type="date" id="endDate" class="p-2 border rounded-md shadow-sm"> <input type="date" id="endDate" class="form-control mt-1">
</div> </div>
</div> </div>
<button id="filterByDateBtn" class="p-2 bg-blue-600 text-white rounded-md shadow-sm">Apply</button> <div class="w-full min-w-[200px] md:flex-1">
<div class="flex-grow"></div> <label for="transactionSearch" class="text-sm font-medium text-slate-600 dark:text-slate-300">Search</label>
<div> <input type="search" id="transactionSearch" placeholder="Descriptions..." class="form-control mt-1">
<label for="transactionSearch" class="text-sm font-medium text-gray-700">Search</label>
<input type="search" id="transactionSearch" placeholder="Descriptions..." class="p-2 border rounded-md shadow-sm">
</div> </div>
<div> <div class="w-full min-w-[160px] md:w-auto">
<label for="transactionFilter" class="text-sm font-medium text-gray-700">Status</label> <label for="transactionFilter" class="text-sm font-medium text-slate-600 dark:text-slate-300">Status</label>
<select id="transactionFilter" class="p-2 border rounded-md shadow-sm"> <select id="transactionFilter" class="form-control mt-1">
<option value="all">All</option> <option value="all">All</option>
<option value="uncategorized">Uncategorized</option> <option value="uncategorized">Uncategorized</option>
</select> </select>
</div> </div>
<div class="w-full md:hidden">
<label for="mobileSortSelect" class="text-sm font-medium text-slate-600 dark:text-slate-300">Sort By</label>
<select id="mobileSortSelect" class="form-control mt-1">
<option value="date-desc" selected>Newest first</option>
<option value="date-asc">Oldest first</option>
<option value="amount-desc">Amount (high -> low)</option>
<option value="amount-asc">Amount (low -> high)</option>
<option value="description-asc">Description (A -> Z)</option>
<option value="description-desc">Description (Z -> A)</option>
<option value="category-asc">Category (A -> Z)</option>
<option value="category-desc">Category (Z -> A)</option>
<option value="vendorName-asc">Vendor (A -> Z)</option>
<option value="vendorName-desc">Vendor (Z -> A)</option>
</select>
</div>
</div> </div>
</div> </div>
<div class="bg-white p-4 rounded-lg shadow-md overflow-x-auto"> <div id="transactionsListMobile" class="space-y-4 md:hidden">
<table class="w-full"> </div>
<thead class="border-b"> <div class="card hidden overflow-hidden md:block">
<tr> <div class="overflow-x-auto">
<th class="p-3 text-left sortable-header" data-sort="date">Date</th> <table class="min-w-full divide-y divide-slate-200 text-left text-sm dark:divide-slate-800">
<th class="p-3 text-left sortable-header" data-sort="description">Description</th> <thead class="bg-slate-50/80 text-xs font-semibold uppercase tracking-wide text-slate-500 dark:bg-slate-900/40 dark:text-slate-300">
<th class="p-3 text-left sortable-header" data-sort="category">Category</th> <tr>
<th class="p-3 text-left sortable-header" data-sort="vendorName">Vendor</th> <th class="sortable-header px-4 py-3" data-sort="date">Date</th>
<th class="p-3 text-right sortable-header" data-sort="amount">Amount</th> <th class="sortable-header px-4 py-3" data-sort="description">Description</th>
<th class="p-3 text-center">Actions</th> <th class="sortable-header px-4 py-3" data-sort="category">Category</th>
</tr> <th class="sortable-header px-4 py-3" data-sort="vendorName">Vendor</th>
</thead> <th class="sortable-header px-4 py-3 text-right" data-sort="amount">Amount</th>
<tbody id="transactionsTableBody"> <th class="px-4 py-3 text-center">Actions</th>
</tr>
</thead>
<tbody id="transactionsTableBody" class="divide-y divide-slate-100 dark:divide-slate-800">
</tbody> </tbody>
</table> </table>
</div>
</div> </div>
</section> </section>
<section id="upload" class="hidden"> <section id="upload" class="hidden space-y-6">
<h2 class="text-3xl font-bold mb-6">Upload Files</h2> <div>
<form id="uploadForm" class="bg-white p-6 rounded-lg shadow-md max-w-lg"> <h2 class="text-3xl font-semibold text-slate-900 dark:text-white">Upload Files</h2>
<div class="mb-4"> <p class="mt-2 text-sm text-slate-500 dark:text-slate-400">Keep your books fresh by dropping in your latest CSV exports.</p>
<label for="bankFile" class="block text-sm font-medium text-gray-700 mb-2">1. Bank Transactions CSV</label> </div>
<p class="text-xs text-gray-500 mb-2">(e.g., your `transactions.csv` file. Must have 'Date', 'Description', 'Debit', 'Credit' columns)</p> <form id="uploadForm" class="card max-w-2xl space-y-6">
<input type="file" id="bankFile" accept=".csv" class="w-full p-2 border rounded-md"> <div class="space-y-2">
<label for="bankFile" class="block text-sm font-medium text-slate-600 dark:text-slate-300">1. Bank Transactions CSV</label>
<p class="text-xs text-slate-500 dark:text-slate-400">(e.g., your `transactions.csv` file. Must have 'Date', 'Description', 'Debit', 'Credit' columns)</p>
<input type="file" id="bankFile" accept=".csv" class="form-control cursor-pointer bg-white/60 file:mr-4 file:rounded-full file:border-0 file:bg-blue-500 file:px-4 file:py-2 file:text-sm file:font-semibold file:text-white hover:file:bg-blue-600 dark:bg-slate-800/60">
</div> </div>
<div class="mb-6"> <div class="space-y-2">
<label for="salesFile" class="block text-sm font-medium text-gray-700 mb-2">2. Sales Summary CSV</label> <label for="salesFile" class="block text-sm font-medium text-slate-600 dark:text-slate-300">2. Sales Summary CSV</label>
<p class="text-xs text-gray-500 mb-2">(e.g., your `sales-summary.csv` file. Used to find 'Net sales' and 'Square payment processing fees')</p> <p class="text-xs text-slate-500 dark:text-slate-400">(e.g., your `sales-summary.csv` file. Used to find 'Net sales' and 'Square payment processing fees')</p>
<input type="file" id="salesFile" accept=".csv" class="w-full p-2 border rounded-md"> <input type="file" id="salesFile" accept=".csv" class="form-control cursor-pointer bg-white/60 file:mr-4 file:rounded-full file:border-0 file:bg-indigo-500 file:px-4 file:py-2 file:text-sm file:font-semibold file:text-white hover:file:bg-indigo-600 dark:bg-slate-800/60">
</div> </div>
<button type="submit" class="w-full bg-blue-600 text-white py-3 px-4 rounded-lg font-semibold hover:bg-blue-700"> <button type="submit" class="btn btn-primary w-full bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500 py-3 text-white shadow-lg shadow-indigo-500/30 transition hover:-translate-y-0.5 hover:shadow-xl">
Process Files Process Files
</button> </button>
</form> </form>
</section> </section>
<section id="categories" class="hidden"> <section id="categories" class="hidden space-y-6">
<h2 class="text-3xl font-bold mb-6">Manage Categories</h2> <div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <h2 class="text-3xl font-semibold text-slate-900 dark:text-white">Manage Categories</h2>
<div class="bg-white p-6 rounded-lg shadow-md"> <p class="mt-2 text-sm text-slate-500 dark:text-slate-400">Build the buckets that make sense for your world.</p>
<h3 class="text-xl font-semibold mb-4">Add New Category</h3> </div>
<form id="categoryForm"> <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div class="mb-4"> <div class="card space-y-4">
<label for="categoryName" class="block text-sm font-medium text-gray-700">Name</label> <h3 class="text-xl font-semibold text-slate-900 dark:text-white">Add New Category</h3>
<input type="text" id="categoryName" name="categoryName" required class="mt-1 w-full p-2 border rounded-md shadow-sm"> <form id="categoryForm" class="space-y-4">
<div class="space-y-2">
<label for="categoryName" class="block text-sm font-medium text-slate-600 dark:text-slate-300">Name</label>
<input type="text" id="categoryName" name="categoryName" required class="form-control">
</div> </div>
<div class="mb-4"> <div class="space-y-2">
<label for="categoryType" class="block text-sm font-medium text-gray-700">Type</label> <label for="categoryType" class="block text-sm font-medium text-slate-600 dark:text-slate-300">Type</label>
<select id="categoryType" name="categoryType" class="mt-1 w-full p-2 border rounded-md shadow-sm"> <select id="categoryType" name="categoryType" class="form-control">
<option value="income">Income</option> <option value="income">Income</option>
<option value="expense">Expense</option> <option value="expense">Expense</option>
<option value="liability">Liability</option> <option value="liability">Liability</option>
</select> </select>
</div> </div>
<button type="submit" class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg font-semibold hover:bg-blue-700"> <button type="submit" class="btn btn-primary w-full bg-gradient-to-r from-blue-500 to-indigo-500 py-2 text-white shadow-lg shadow-indigo-500/30 transition hover:-translate-y-0.5 hover:shadow-xl">
Add Category Add Category
</button> </button>
</form> </form>
</div> </div>
<div class="bg-white p-6 rounded-lg shadow-md"> <div class="card space-y-4">
<h3 class="text-xl font-semibold mb-4">Existing Categories</h3> <h3 class="text-xl font-semibold text-slate-900 dark:text-white">Existing Categories</h3>
<div id="categoryList" class="space-y-2 max-h-96 overflow-y-auto"> <div id="categoryList" class="space-y-2 max-h-96 overflow-y-auto">
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<section id="vendors" class="hidden"> <section id="vendors" class="hidden space-y-6">
<h2 class="text-3xl font-bold mb-6">Manage Vendors</h2> <div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <h2 class="text-3xl font-semibold text-slate-900 dark:text-white">Vendors</h2>
<div class="bg-white p-6 rounded-lg shadow-md"> <p class="mt-2 text-sm text-slate-500 dark:text-slate-400">Keep track of who helps your business shine.</p>
<h3 class="text-xl font-semibold mb-4">Add New Vendor</h3> </div>
<form id="vendorForm"> <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div class="mb-4"> <div class="card space-y-4">
<label for="vendorName" class="block text-sm font-medium text-gray-700">Name</label> <h3 class="text-xl font-semibold text-slate-900 dark:text-white">Add New Vendor</h3>
<input type="text" id="vendorName" name="vendorName" required class="mt-1 w-full p-2 border rounded-md shadow-sm"> <form id="vendorForm" class="space-y-4">
<div class="space-y-2">
<label for="vendorName" class="block text-sm font-medium text-slate-600 dark:text-slate-300">Vendor Name</label>
<input type="text" id="vendorName" name="vendorName" required class="form-control">
</div> </div>
<button type="submit" class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg font-semibold hover:bg-blue-700"> <div class="space-y-2">
<label for="vendorContact" class="block text-sm font-medium text-slate-600 dark:text-slate-300">Contact Info</label>
<input type="text" id="vendorContact" name="vendorContact" class="form-control">
</div>
<button type="submit" class="btn btn-primary w-full bg-gradient-to-r from-blue-500 to-indigo-500 py-2 text-white shadow-lg shadow-indigo-500/30 transition hover:-translate-y-0.5 hover:shadow-xl">
Add Vendor Add Vendor
</button> </button>
</form> </form>
</div> </div>
<div class="bg-white p-6 rounded-lg shadow-md"> <div class="card space-y-4">
<h3 class="text-xl font-semibold mb-4">Existing Vendors</h3> <h3 class="text-xl font-semibold text-slate-900 dark:text-white">Existing Vendors</h3>
<div id="vendorList" class="space-y-2 max-h-96 overflow-y-auto"> <div id="vendorList" class="space-y-2 max-h-96 overflow-y-auto">
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<section id="rules" class="hidden"> <section id="rules" class="hidden space-y-6">
<div class="flex justify-between items-center mb-6"> <div class="flex flex-wrap items-center justify-between gap-4">
<h2 class="text-3xl font-bold">Categorization Rules</h2> <div>
<button id="applyAllRulesBtn" class="bg-green-600 text-white py-2 px-4 rounded-lg font-semibold hover:bg-green-700"> <h2 class="text-3xl font-semibold text-slate-900 dark:text-white">Categorization Rules</h2>
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">Teach SimpleLedger how to sort new transactions automatically.</p>
</div>
<button id="applyAllRulesBtn" class="btn btn-primary bg-gradient-to-r from-emerald-500 to-teal-500 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-emerald-500/30 transition hover:-translate-y-0.5 hover:shadow-xl">
Apply Rules to Uncategorized Apply Rules to Uncategorized
</button> </button>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div class="bg-white p-6 rounded-lg shadow-md"> <div class="card space-y-4">
<h3 class="text-xl font-semibold mb-4">Add New Rule</h3> <h3 class="text-xl font-semibold text-slate-900 dark:text-white">Add New Rule</h3>
<form id="ruleForm"> <form id="ruleForm" class="space-y-4">
<div class="mb-4"> <div class="space-y-2">
<label for="ruleKeyword" class="block text-sm font-medium text-gray-700">If 'Description' contains:</label> <label for="ruleKeyword" class="block text-sm font-medium text-slate-600 dark:text-slate-300">If 'Description' contains:</label>
<input type="text" id="ruleKeyword" name="ruleKeyword" placeholder="e.g., ADP PAYROLL" required class="mt-1 w-full p-2 border rounded-md shadow-sm"> <input type="text" id="ruleKeyword" name="ruleKeyword" placeholder="e.g., ADP PAYROLL" required class="form-control">
</div> </div>
<div class="mb-4"> <div class="space-y-2">
<label for="ruleCategory" class="block text-sm font-medium text-gray-700">Set Category to:</label> <label for="ruleCategory" class="block text-sm font-medium text-slate-600 dark:text-slate-300">Set Category to:</label>
<select id="ruleCategory" name="ruleCategory" class="category-select mt-1 w-full p-2 border rounded-md shadow-sm"> <select id="ruleCategory" name="ruleCategory" class="category-select form-control">
</select> </select>
</div> </div>
<button type="submit" class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg font-semibold hover:bg-blue-700"> <button type="submit" class="btn btn-primary w-full bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500 py-2 text-white shadow-lg shadow-indigo-500/30 transition hover:-translate-y-0.5 hover:shadow-xl">
Add Rule Add Rule
</button> </button>
</form> </form>
</div> </div>
<div class="bg-white p-6 rounded-lg shadow-md"> <div class="card space-y-4">
<h3 class="text-xl font-semibold mb-4">Existing Rules</h3> <h3 class="text-xl font-semibold text-slate-900 dark:text-white">Existing Rules</h3>
<div id="rulesList" class="space-y-2 max-h-96 overflow-y-auto"> <div id="rulesList" class="space-y-2 max-h-96 overflow-y-auto">
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<section id="reports" class="hidden"> <section id="reports" class="hidden space-y-6">
<h2 class="text-3xl font-bold mb-6">Reports</h2> <div>
<div class="bg-white p-6 rounded-lg shadow-md"> <h2 class="text-3xl font-semibold text-slate-900 dark:text-white">Reports</h2>
<h3 class="text-xl font-semibold mb-4">Profit & Loss Statement</h3> <p class="mt-2 text-sm text-slate-500 dark:text-slate-400">Build your Profit &amp; Loss in seconds.</p>
<div class="flex items-center space-x-4 mb-4"> </div>
<div> <div class="card space-y-6">
<label for="reportStartDate" class="block text-sm font-medium text-gray-700">Start Date</label> <div class="flex flex-wrap items-end gap-4">
<input type="date" id="reportStartDate" class="mt-1 p-2 border rounded-md shadow-sm"> <div class="min-w-[160px] space-y-2">
<label for="reportStartDate" class="block text-sm font-medium text-slate-600 dark:text-slate-300">Start Date</label>
<input type="date" id="reportStartDate" class="form-control">
</div> </div>
<div> <div class="min-w-[160px] space-y-2">
<label for="reportEndDate" class="block text-sm font-medium text-gray-700">End Date</label> <label for="reportEndDate" class="block text-sm font-medium text-slate-600 dark:text-slate-300">End Date</label>
<input type="date" id="reportEndDate" class="mt-1 p-2 border rounded-md shadow-sm"> <input type="date" id="reportEndDate" class="form-control">
</div> </div>
<button id="generateReportBtn" class="self-end bg-blue-600 text-white py-2 px-4 rounded-lg font-semibold hover:bg-blue-700"> <button id="generateReportBtn" class="btn btn-primary bg-gradient-to-r from-blue-500 to-indigo-500 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-indigo-500/30 transition hover:-translate-y-0.5 hover:shadow-xl">
Generate Generate
</button> </button>
<button id="exportReportBtn" class="hidden self-end bg-green-600 text-white py-2 px-4 rounded-lg font-semibold hover:bg-green-700"> <button id="exportReportBtn" class="hidden btn bg-gradient-to-r from-emerald-500 to-teal-500 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-emerald-500/30 transition hover:-translate-y-0.5 hover:shadow-xl">
Export to CSV Export to CSV
</button> </button>
</div> </div>
<div class="overflow-x-auto">
<table class="w-full max-w-2xl mt-6"> <table class="min-w-full divide-y divide-slate-200 text-left text-sm dark:divide-slate-800">
<tbody id="plReportBody"> <tbody id="plReportBody" class="divide-y divide-slate-100 dark:divide-slate-800">
</tbody> </tbody>
</table> </table>
</div>
</div> </div>
</section> </section>
</main> </main>
</div> </div>
</div>
</div>
<script src="app.js"></script> <script src="app.js"></script>