${formatCurrency(e.amount)}
`;
});
if(recentList.innerHTML === '') recentList.innerHTML = `
No recent expenses logged.
`;
renderCategoryChart();
}
function renderCategoryChart() {
const ctx = document.getElementById('categoryChart').getContext('2d');
const spendByCategory = state.categories.reduce((acc, cat) => {
acc[cat] = state.expenses.filter(e => e.category === cat).reduce((sum, e) => sum + e.amount, 0);
return acc;
}, {});
const chartData = {
labels: Object.keys(spendByCategory),
datasets: [{ data: Object.values(spendByCategory), backgroundColor: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'], borderColor: '#FFFFFF', borderWidth: 2 }]
};
if (categoryChart) { categoryChart.data = chartData; categoryChart.update(); }
else { categoryChart = new Chart(ctx, { type: 'doughnut', data: chartData, options: { responsive: true, plugins: { legend: { position: 'bottom' } } } }); }
}
function renderExpenseTable() {
const tableBody = document.getElementById('expense-table-body');
tableBody.innerHTML = '';
if(state.expenses.length === 0) { tableBody.innerHTML = `
| No expenses logged. |
`; return; }
state.expenses.sort((a,b) => new Date(b.date) - new Date(a.date)).forEach(e => {
tableBody.innerHTML += `
| ${e.date} |
${e.matter} |
${e.category} |
${formatCurrency(e.amount)} |
|
`;
});
}
function renderConfigLists() {
const configMap = { category: state.categories, matter: state.matters, provider: state.providers };
for (const [type, items] of Object.entries(configMap)) {
document.getElementById(`${type}-list`).innerHTML = items.map(item => `
${item}`).join('');
}
}
function updateSelectOptions() {
const selects = { 'expense-category': state.categories, 'expense-matter': state.matters, 'expense-provider': state.providers };
for (const [id, options] of Object.entries(selects)) {
const el = document.getElementById(id);
if(el) el.innerHTML = options.map(o => `
`).join('');
}
}
// --- EVENT HANDLERS ---
Object.keys({category:0, matter:0, provider:0}).forEach(type => {
document.getElementById(`${type}-form`).addEventListener('submit', (e) => {
e.preventDefault();
const input = e.target.querySelector('input');
const value = input.value.trim();
if (value && !state[`${type}s`].includes(value)) {
state[`${type}s`].push(value);
input.value = '';
saveState(); renderAll();
}
});
});
document.getElementById('expense-form').addEventListener('submit', (e) => {
e.preventDefault();
const id = document.getElementById('expense-id').value;
const data = {
date: document.getElementById('expense-date').value, amount: parseFloat(document.getElementById('expense-amount').value),
matter: document.getElementById('expense-matter').value, category: document.getElementById('expense-category').value,
provider: document.getElementById('expense-provider').value, description: document.getElementById('expense-description').value,
};
if (id) {
const index = state.expenses.findIndex(ex => ex.id == id);
if (index > -1) state.expenses[index] = { ...state.expenses[index], ...data };
} else {
data.id = state.nextExpenseId++;
state.expenses.push(data);
}
saveState(); renderAll(); app.closeExpenseModal();
});
// --- GLOBAL APP OBJECT ---
window.app = {
switchTab: (tabName) => {
state.currentTab = tabName;
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active', 'border-blue-500', 'text-blue-600'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.getElementById(`tab-btn-${tabName}`).classList.add('active', 'border-blue-500', 'text-blue-600');
document.getElementById(`tab-content-${tabName}`).classList.add('active');
},
navigateTabs: (dir) => {
const i = tabOrder.indexOf(state.currentTab);
const nextI = dir === 'next' ? (i + 1) % tabOrder.length : (i - 1 + tabOrder.length) % tabOrder.length;
app.switchTab(tabOrder[nextI]);
},
openExpenseModal: (id = null) => {
document.getElementById('expense-form').reset();
document.getElementById('expense-id').value = '';
if (id) {
const expense = state.expenses.find(e => e.id === id);
if(expense) {
document.getElementById('modal-title').textContent = 'Edit Expense';
Object.keys(expense).forEach(key => {
const el = document.getElementById(`expense-${key === 'id' ? 'id' : key}`);
if (el) el.value = expense[key];
});
}
} else { document.getElementById('modal-title').textContent = 'Add New Expense'; }
document.getElementById('expense-modal').classList.remove('hidden');
},
closeExpenseModal: () => document.getElementById('expense-modal').classList.add('hidden'),
editExpense: (id) => app.openExpenseModal(id),
deleteExpense: (id) => {
if (confirm('Are you sure you want to delete this expense?')) {
state.expenses = state.expenses.filter(e => e.id !== id);
saveState(); renderAll();
}
},
deleteConfigItem: (type, value) => {
const keyMap = { category: 'category', matter: 'matter', provider: 'provider' };
if (state.expenses.some(e => e[keyMap[type]] === value)) { alert(`Cannot delete. This ${type} is in use.`); return; }
state[`${type}s`] = state[`${type}s`].filter(item => item !== value);
saveState(); renderAll();
},
generatePDF: () => {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
if (typeof doc.autoTable !== 'function') { console.error("jsPDF-AutoTable not loaded."); return; }
doc.setFontSize(18); doc.text("Legal Expense Report", 14, 22);
doc.setFontSize(11); doc.text(`Generated: ${new Date().toLocaleDateString()}`, 14, 30);
doc.autoTable({
startY: 40, theme: 'striped',
body: [
['Total Spend (YTD)', document.getElementById('stat-total-spend').textContent],
['Active Matters', document.getElementById('stat-active-matters').textContent],
['Average Cost per Matter', document.getElementById('stat-avg-cost').textContent],
]
});
const tableData = state.expenses.map(e => [ e.date, e.matter, e.category, e.provider, formatCurrency(e.amount) ]);
doc.autoTable({
startY: doc.lastAutoTable.finalY + 15, theme: 'grid',
head: [['Date', 'Matter', 'Category', 'Provider', 'Amount']],
body: tableData,
});
doc.save('Legal-Expense-Report.pdf');
}
};
// Initial Load
loadState(); renderAll(); app.switchTab(state.currentTab);
});