update mobile experiance
This commit is contained in:
parent
bfb1ca62ce
commit
5985375755
336
app.js
336
app.js
@ -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">×</button>
|
<button data-id="${cat.id}" class="text-red-500 hover:text-red-700">×</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">×</button>
|
<button data-id="${v.id}" class="text-red-500 hover:text-red-700">×</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">×</button>
|
<button data-id="${rule.id}" class="text-red-500 hover:text-red-700">×</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,53 +302,80 @@
|
|||||||
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';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = `
|
tr.innerHTML = `
|
||||||
<td class="p-3">${txDate.toLocaleDateString()}</td>
|
<td class="p-3">${txDate.toLocaleDateString()}</td>
|
||||||
<td class="p-3 truncate max-w-xs">${tx.description}</td>
|
<td class="p-3 truncate max-w-xs">${tx.description}</td>
|
||||||
@ -357,8 +390,42 @@
|
|||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(tr);
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (tbody) {
|
||||||
tbody.querySelectorAll('.edit-tx-btn').forEach(btn => {
|
tbody.querySelectorAll('.edit-tx-btn').forEach(btn => {
|
||||||
btn.onclick = (e) => showSplitTransactionModal(e.target.dataset.id);
|
btn.onclick = (e) => showSplitTransactionModal(e.target.dataset.id);
|
||||||
});
|
});
|
||||||
@ -367,6 +434,16 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
const column = e.target.dataset.sort;
|
const column = e.target.dataset.sort;
|
||||||
if (!column) return;
|
if (!column) return;
|
||||||
@ -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 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');
|
const customPicker = document.getElementById('customDateRangePicker');
|
||||||
|
if (!customPicker) return;
|
||||||
|
transactionDateFilter = e.target.value;
|
||||||
if (e.target.value === 'custom') {
|
if (e.target.value === 'custom') {
|
||||||
customPicker.classList.remove('hidden');
|
customPicker.classList.remove('hidden');
|
||||||
customPicker.classList.add('flex');
|
customPicker.classList.add('flex');
|
||||||
|
handleDateChange();
|
||||||
} else {
|
} else {
|
||||||
customPicker.classList.add('hidden');
|
customPicker.classList.add('hidden');
|
||||||
customPicker.classList.remove('flex');
|
customPicker.classList.remove('flex');
|
||||||
|
customDateRange = { start: null, end: null };
|
||||||
|
renderTransactions();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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();
|
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();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mobile menu
|
const startDateInput = document.getElementById('startDate');
|
||||||
const sidebar = document.getElementById('sidebar');
|
const endDateInput = document.getElementById('endDate');
|
||||||
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
|
if (startDateInput) startDateInput.onchange = handleDateChange;
|
||||||
|
if (endDateInput) endDateInput.onchange = handleDateChange;
|
||||||
|
|
||||||
if (mobileMenuBtn) {
|
const mobileSortSelectEl = document.getElementById('mobileSortSelect');
|
||||||
mobileMenuBtn.onclick = () => {
|
if (mobileSortSelectEl) {
|
||||||
sidebar.classList.toggle('-translate-x-full');
|
mobileSortSelectEl.onchange = (e) => {
|
||||||
sidebar.classList.toggle('hidden');
|
const [column, direction] = e.target.value.split('-');
|
||||||
|
transactionSort.column = column;
|
||||||
|
transactionSort.direction = direction;
|
||||||
|
renderTransactions();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Transaction table sorting
|
// Transaction table sorting
|
||||||
document.querySelectorAll('.sortable-header').forEach(th => {
|
document.querySelectorAll('.sortable-header').forEach(th => {
|
||||||
th.onclick = handleSort;
|
th.onclick = handleSort;
|
||||||
|
|||||||
566
index.html
566
index.html
@ -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">
|
<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">
|
||||||
<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>
|
<div class="flex items-start justify-between gap-4 px-1">
|
||||||
|
<div>
|
||||||
|
<span class="text-xs font-semibold uppercase tracking-[0.4em] text-blue-500">SimpleLedger</span>
|
||||||
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<nav class="flex-1 space-y-2">
|
||||||
<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">
|
<a href="#" data-view="dashboard" class="nav-link">
|
||||||
<h1 class="text-2xl font-bold mb-6">SimpleLedger</h1>
|
<span class="nav-icon bg-blue-500/10 text-blue-500">
|
||||||
<a href="#" data-view="dashboard" class="block p-3 rounded-lg text-blue-100 hover:bg-blue-600">Dashboard</a>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<a href="#" data-view="transactions" class="block p-3 rounded-lg text-blue-100 hover:bg-blue-600">Transactions</a>
|
<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" />
|
||||||
<a href="#" data-view="upload" class="block p-3 rounded-lg text-blue-100 hover:bg-blue-600">Upload</a>
|
</svg>
|
||||||
<a href="#" data-view="categories" class="block p-3 rounded-lg text-blue-100 hover:bg-blue-600">Categories</a>
|
</span>
|
||||||
<a href="#" data-view="vendors" class="block p-3 rounded-lg text-blue-100 hover:bg-blue-600">Vendors</a>
|
<span>Dashboard</span>
|
||||||
<a href="#" data-view="rules" class="block p-3 rounded-lg text-blue-100 hover:bg-blue-600">Rules</a>
|
</a>
|
||||||
<a href="#" data-view="reports" class="block p-3 rounded-lg text-blue-100 hover:bg-blue-600">Reports</a>
|
<a href="#" data-view="transactions" class="nav-link">
|
||||||
<div class="mt-auto">
|
<span class="nav-icon bg-indigo-500/10 text-indigo-500">
|
||||||
<a href="#" id="logoutButton" class="p-3 rounded-lg text-blue-100 hover:bg-blue-600 block">Logout</a>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</div>
|
<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>
|
</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">
|
||||||
|
|
||||||
<main class="flex-1 p-4 md:p-8 overflow-y-auto">
|
|
||||||
|
|
||||||
<section id="dashboard">
|
<section id="dashboard" class="space-y-6">
|
||||||
<h2 class="text-3xl font-bold mb-6">Dashboard</h2>
|
<div>
|
||||||
|
<h2 class="text-3xl font-semibold text-slate-900 dark:text-white">Dashboard</h2>
|
||||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-6 mb-6">
|
<p class="mt-2 text-sm text-slate-500 dark:text-slate-400">Get a pulse on your business at a glance.</p>
|
||||||
<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>
|
||||||
<div class="bg-white p-6 rounded-lg shadow-md">
|
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
|
||||||
<h3 class="text-sm font-medium text-gray-500">Returns & Discounts</h3>
|
<div class="card stat-card">
|
||||||
<p id="statReturns" class="text-2xl font-semibold text-yellow-600">$0.00</p>
|
<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>
|
||||||
<div class="bg-white p-6 rounded-lg shadow-md">
|
<div>
|
||||||
<h3 class="text-sm font-medium text-gray-500">Net Income</h3>
|
<h3 class="stat-label">Gross Income</h3>
|
||||||
<p id="statNetIncome" class="text-2xl font-semibold text-green-700">$0.00</p>
|
<p id="statGrossIncome" class="stat-value text-emerald-500">$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>
|
</div>
|
||||||
|
<div class="card stat-card">
|
||||||
<div class="bg-white p-6 rounded-lg shadow-md h-96">
|
<div class="stat-icon bg-amber-500/15 text-amber-500">
|
||||||
<h3 class="text-xl font-semibold mb-4">Top 10 Expenses</h3>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<div class="relative h-full w-full max-h-[300px]">
|
<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 & 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>
|
<canvas id="expensePieChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="transactions" class="hidden">
|
<section id="transactions" class="hidden space-y-6">
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="card space-y-4">
|
||||||
<h2 class="text-3xl font-bold">Transactions</h2>
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||||
<div class="flex items-end space-x-4">
|
<h2 class="text-2xl font-semibold text-slate-900 dark:text-white">Transactions</h2>
|
||||||
<div>
|
<p class="text-sm text-slate-500 dark:text-slate-400">Filter, search, and split with ease.</p>
|
||||||
<label for="dateFilterSelect" class="text-sm font-medium text-gray-700">Date Range</label>
|
</div>
|
||||||
<select id="dateFilterSelect" class="p-2 border rounded-md shadow-sm">
|
<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 class="bg-white p-4 rounded-lg shadow-md overflow-x-auto">
|
</div>
|
||||||
<table class="w-full">
|
<div id="transactionsListMobile" class="space-y-4 md:hidden">
|
||||||
<thead class="border-b">
|
</div>
|
||||||
|
<div class="card hidden overflow-hidden md:block">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-slate-200 text-left text-sm dark:divide-slate-800">
|
||||||
|
<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">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="p-3 text-left sortable-header" data-sort="date">Date</th>
|
<th class="sortable-header px-4 py-3" data-sort="date">Date</th>
|
||||||
<th class="p-3 text-left sortable-header" data-sort="description">Description</th>
|
<th class="sortable-header px-4 py-3" data-sort="description">Description</th>
|
||||||
<th class="p-3 text-left sortable-header" data-sort="category">Category</th>
|
<th class="sortable-header px-4 py-3" data-sort="category">Category</th>
|
||||||
<th class="p-3 text-left sortable-header" data-sort="vendorName">Vendor</th>
|
<th class="sortable-header px-4 py-3" data-sort="vendorName">Vendor</th>
|
||||||
<th class="p-3 text-right sortable-header" data-sort="amount">Amount</th>
|
<th class="sortable-header px-4 py-3 text-right" data-sort="amount">Amount</th>
|
||||||
<th class="p-3 text-center">Actions</th>
|
<th class="px-4 py-3 text-center">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="transactionsTableBody">
|
<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>
|
|
||||||
<form id="categoryForm">
|
|
||||||
<div class="mb-4">
|
|
||||||
<label for="categoryName" class="block text-sm font-medium text-gray-700">Name</label>
|
|
||||||
<input type="text" id="categoryName" name="categoryName" required class="mt-1 w-full p-2 border rounded-md shadow-sm">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||||
<label for="categoryType" class="block text-sm font-medium text-gray-700">Type</label>
|
<div class="card space-y-4">
|
||||||
<select id="categoryType" name="categoryType" class="mt-1 w-full p-2 border rounded-md shadow-sm">
|
<h3 class="text-xl font-semibold text-slate-900 dark:text-white">Add New Category</h3>
|
||||||
|
<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 class="space-y-2">
|
||||||
|
<label for="categoryType" class="block text-sm font-medium text-slate-600 dark:text-slate-300">Type</label>
|
||||||
|
<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>
|
|
||||||
<form id="vendorForm">
|
|
||||||
<div class="mb-4">
|
|
||||||
<label for="vendorName" class="block text-sm font-medium text-gray-700">Name</label>
|
|
||||||
<input type="text" id="vendorName" name="vendorName" required class="mt-1 w-full p-2 border rounded-md shadow-sm">
|
|
||||||
</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="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||||
|
<div class="card space-y-4">
|
||||||
|
<h3 class="text-xl font-semibold text-slate-900 dark:text-white">Add New Vendor</h3>
|
||||||
|
<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 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 class="bg-white p-6 rounded-lg shadow-md">
|
|
||||||
<h3 class="text-xl font-semibold mb-4">Profit & Loss Statement</h3>
|
|
||||||
<div class="flex items-center space-x-4 mb-4">
|
|
||||||
<div>
|
<div>
|
||||||
<label for="reportStartDate" class="block text-sm font-medium text-gray-700">Start Date</label>
|
<h2 class="text-3xl font-semibold text-slate-900 dark:text-white">Reports</h2>
|
||||||
<input type="date" id="reportStartDate" class="mt-1 p-2 border rounded-md shadow-sm">
|
<p class="mt-2 text-sm text-slate-500 dark:text-slate-400">Build your Profit & Loss in seconds.</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="card space-y-6">
|
||||||
<label for="reportEndDate" class="block text-sm font-medium text-gray-700">End Date</label>
|
<div class="flex flex-wrap items-end gap-4">
|
||||||
<input type="date" id="reportEndDate" 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>
|
||||||
<button id="generateReportBtn" class="self-end bg-blue-600 text-white py-2 px-4 rounded-lg font-semibold hover:bg-blue-700">
|
<div class="min-w-[160px] space-y-2">
|
||||||
|
<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="form-control">
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user