`).join('');
// Update vendor dropdown in invoice form
invoiceVendorInput.innerHTML = vendors.map(v => `
`).join('');
}
// --- INVOICE LOGIC --- //
function toggleInvoiceForm(show, invoice = null) {
invoiceFormContainer.classList.toggle('hidden', !show);
if (show) {
invoiceFormTitle.textContent = invoice ? 'Edit Invoice' : 'Add New Invoice';
invoiceIdInput.value = invoice ? invoice.id : '';
invoiceVendorInput.value = invoice ? invoice.vendorId : (vendors[0]?.id || '');
invoiceNumberInput.value = invoice ? invoice.number : '';
invoiceAmountInput.value = invoice ? invoice.amount : '';
invoiceDueDateInput.value = invoice ? invoice.dueDate : '';
invoiceStatusInput.value = invoice ? invoice.status : 'Draft';
}
}
function handleSaveInvoice() {
const id = invoiceIdInput.value;
const invoiceData = {
id: id ? parseInt(id) : Date.now(),
vendorId: parseInt(invoiceVendorInput.value),
number: invoiceNumberInput.value.trim(),
amount: parseFloat(invoiceAmountInput.value),
dueDate: invoiceDueDateInput.value,
status: invoiceStatusInput.value
};
if (!invoiceData.vendorId || !invoiceData.number || isNaN(invoiceData.amount) || !invoiceData.dueDate) {
showNotification('Please fill all invoice fields correctly.', 'warning');
return;
}
if (id) { // Update
const index = invoices.findIndex(i => i.id === invoiceData.id);
if (index > -1) invoices[index] = invoiceData;
} else { // Create
invoices.push(invoiceData);
}
saveDataToStorage();
renderInvoices();
updateDashboard();
toggleInvoiceForm(false);
showNotification('Invoice saved successfully!', 'success');
}
function handleInvoiceAction(e) {
const button = e.target.closest('button[data-action]');
if (!button) return;
const id = parseInt(button.dataset.id);
const action = button.dataset.action;
if (action === 'edit') {
const invoice = invoices.find(i => i.id === id);
if (invoice) toggleInvoiceForm(true, invoice);
} else if (action === 'delete') {
if (confirm('Are you sure you want to delete this invoice?')) {
invoices = invoices.filter(i => i.id !== id);
saveDataToStorage();
renderInvoices();
updateDashboard();
}
}
}
function renderInvoices() {
invoicesListEl.innerHTML = invoices.map(invoice => {
const vendor = vendors.find(v => v.id === invoice.vendorId);
const isOverdue = new Date(invoice.dueDate) < new Date() && invoice.status !== 'Paid';
const statusColors = {
Paid: 'bg-green-100 text-green-800',
Sent: 'bg-blue-100 text-blue-800',
Draft: 'bg-gray-100 text-gray-800',
Overdue: 'bg-red-100 text-red-800'
};
const statusText = isOverdue ? 'Overdue' : invoice.status;
const statusColor = isOverdue ? statusColors.Overdue : statusColors[invoice.status];
return `
| ${vendor ? vendor.name : 'Unknown Vendor'} |
${invoice.number} |
$${invoice.amount.toFixed(2)} |
${invoice.dueDate} |
${statusText} |
|
`;
}).join('');
}
// --- DASHBOARD --- //
function updateDashboard() {
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
let totalOverdue = 0;
let totalOutstanding = 0;
let paidRecent = 0;
let statusCounts = { Draft: 0, Sent: 0, Paid: 0, Overdue: 0 };
invoices.forEach(inv => {
const isOverdue = new Date(inv.dueDate) < today && inv.status !== 'Paid';
if (isOverdue) {
totalOverdue += inv.amount;
statusCounts.Overdue++;
} else {
statusCounts[inv.status]++;
}
if (inv.status !== 'Paid') {
totalOutstanding += inv.amount;
}
if (inv.status === 'Paid') { // Assuming a paidDate property would exist in a real app
paidRecent += inv.amount; // Simplified for this example
}
});
totalOverdueStat.textContent = `$${totalOverdue.toFixed(2)}`;
totalOutstandingStat.textContent = `$${totalOutstanding.toFixed(2)}`;
paidRecentStat.textContent = `$${paidRecent.toFixed(2)}`;
renderStatusChart(statusCounts);
}
function renderStatusChart(counts) {
const ctx = document.getElementById('statusChart');
if (!ctx) return;
const data = {
labels: ['Paid', 'Sent', 'Overdue', 'Draft'],
datasets: [{
data: [counts.Paid, counts.Sent, counts.Overdue, counts.Draft],
backgroundColor: ['#10B981', '#3B82F6', '#EF4444', '#6B7280'],
hoverOffset: 4
}]
};
if (statusChart) statusChart.destroy();
statusChart = new Chart(ctx, {
type: 'doughnut',
data: data,
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } }
});
}
// --- PDF GENERATION --- //
function generatePDF() {
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({ unit: 'pt', format: 'a4' });
let y = 40;
const margin = 40;
const pdfWidth = pdf.internal.pageSize.getWidth();
pdf.setFontSize(20);
pdf.setFont('helvetica', 'bold');
pdf.text('Financial Summary Report', pdfWidth / 2, y, { align: 'center' });
y += 20;
pdf.setFontSize(10);
pdf.setFont('helvetica', 'normal');
pdf.text(new Date().toLocaleDateString(), pdfWidth / 2, y, { align: 'center' });
y += 40;
pdf.setFontSize(12);
pdf.text(`Total Overdue: ${totalOverdueStat.textContent}`, margin, y);
y += 20;
pdf.text(`Total Outstanding: ${totalOutstandingStat.textContent}`, margin, y);
y += 20;
pdf.text(`Paid (Last 30 Days): ${paidRecentStat.textContent}`, margin, y);
y += 40;
const headers = [['Vendor', 'Invoice #', 'Amount', 'Due Date', 'Status']];
const body = invoices.filter(i => i.status !== 'Paid').map(i => {
const vendor = vendors.find(v => v.id === i.vendorId);
const isOverdue = new Date(i.dueDate) < new Date() && i.status !== 'Paid';
return [vendor ? vendor.name : 'N/A', i.number, `$${i.amount.toFixed(2)}`, i.dueDate, isOverdue ? 'Overdue' : i.status];
});
pdf.autoTable({
startY: y,
head: headers,
body: body,
theme: 'grid',
headStyles: { fillColor: [17, 24, 39] }
});
pdf.save('Financial-Report.pdf');
}
// --- TAB NAVIGATION --- //
function setActiveTab(index) {
tabs.forEach((tab, i) => tab.classList.toggle('active', i === index));
tabPanels.forEach((panel, i) => panel.classList.toggle('hidden', i !== index));
if (index === 0) updateDashboard(); // Refresh dashboard on view
}
// --- KICK IT OFF --- //
initializeApp();
});