View file File name : invoices.php Content :<?php // No login required require_once '_helpers.php'; // --- DATA FETCHING AND FILTERING --- try { $conn = get_db_connection(); $business = $conn->query("SELECT * FROM business_settings LIMIT 1")->fetch_assoc(); // --- Fetch All Customers for Filter --- $customers_result = $conn->query("SELECT id, name FROM customers ORDER BY name ASC"); $customers_list = []; if($customers_result) { while ($row = $customers_result->fetch_assoc()) { $customers_list[] = $row; } } // --- Calculate Stats --- $stats_result = $conn->query(" SELECT SUM(CASE WHEN status = 'Overdue' THEN total - amount_paid ELSE 0 END) as total_overdue, SUM(CASE WHEN status = 'Due' AND due_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 30 DAY) THEN total - amount_paid ELSE 0 END) as due_next_30_days, AVG(CASE WHEN status = 'Paid' AND paid_at IS NOT NULL THEN DATEDIFF(paid_at, invoice_date) ELSE NULL END) as avg_days_to_pay FROM invoices "); $stats = $stats_result ? $stats_result->fetch_assoc() : ['total_overdue' => 0, 'due_next_30_days' => 0, 'avg_days_to_pay' => 0]; // --- Tab Counts --- $unpaid_count_result = $conn->query("SELECT COUNT(*) as count FROM invoices WHERE status IN ('Due', 'Overdue', 'Partially Paid')"); $unpaid_count = $unpaid_count_result ? $unpaid_count_result->fetch_assoc()['count'] : 0; $draft_count_result = $conn->query("SELECT COUNT(*) as count FROM invoices WHERE status = 'Draft'"); $draft_count = $draft_count_result ? $draft_count_result->fetch_assoc()['count'] : 0; // --- Build Invoice Query --- $sql = "SELECT i.id, i.status, i.due_date, i.invoice_date, i.total, i.amount_paid, c.name as customer_name FROM invoices i JOIN customers c ON i.customer_id = c.id"; $where_clauses = []; $params = []; $types = ''; // Handle Tabs $active_tab = $_GET['tab'] ?? 'unpaid'; if ($active_tab === 'unpaid') { $where_clauses[] = "i.status IN ('Due', 'Overdue', 'Partially Paid')"; } elseif ($active_tab === 'draft') { $where_clauses[] = "i.status = 'Draft'"; } // Handle Filters if (!empty($_GET['customer']) && $_GET['customer'] !== 'all') { $where_clauses[] = "i.customer_id = ?"; $params[] = $_GET['customer']; $types .= 's'; } if (!empty($_GET['status']) && $_GET['status'] !== 'all') { $where_clauses[] = "i.status = ?"; $params[] = $_GET['status']; $types .= 's'; } if (!empty($_GET['from_date'])) { $where_clauses[] = "i.invoice_date >= ?"; $params[] = $_GET['from_date']; $types .= 's'; } if (!empty($_GET['to_date'])) { $where_clauses[] = "i.invoice_date <= ?"; $params[] = $_GET['to_date']; $types .= 's'; } if (!empty($_GET['invoice_number'])) { $where_clauses[] = "i.id LIKE ?"; $params[] = '%' . $_GET['invoice_number'] . '%'; $types .= 's'; } if (count($where_clauses) > 0) { $sql .= " WHERE " . implode(' AND ', $where_clauses); } $sql .= " ORDER BY i.invoice_date DESC"; $stmt = $conn->prepare($sql); if ($stmt) { if (count($params) > 0) { $stmt->bind_param($types, ...$params); } $stmt->execute(); $invoices = fetch_all_assoc($stmt); $stmt->close(); } else { $invoices = []; } $conn->close(); } catch (Exception $e) { die("An error occurred: " . $e->getMessage()); } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Invoices - <?php echo htmlspecialchars($business['name']); ?></title> <script src="https://cdn.tailwindcss.com"></script> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> <style> body { font-family: 'Inter', sans-serif; } :root { --primary: <?php echo htmlspecialchars($business['primary_color'] ?? '217 91% 60%'); ?>; } .bg-primary { background-color: hsl(var(--primary)); } .text-primary { color: hsl(var(--primary)); } .border-primary { border-color: hsl(var(--primary)); } .ring-primary:focus-visible { --tw-ring-color: hsl(var(--primary)); } .tab-active { border-color: hsl(var(--primary)); color: hsl(var(--primary)); } </style> </head> <body class="bg-slate-50"> <div class="flex h-screen"> <?php include '_sidebar.php'; ?> <div class="flex-1 flex flex-col overflow-hidden lg:ml-64"> <?php include '_header.php'; ?> <main class="flex-1 overflow-x-hidden overflow-y-auto bg-slate-50 p-4 sm:p-6 lg:p-8"> <div class="flex flex-col gap-8"> <div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4"> <div> <h1 class="text-3xl font-bold tracking-tight">Invoices</h1> </div> <a href="invoice-create.php" class="w-full sm:w-auto bg-primary text-white font-semibold py-2 px-4 rounded-full hover:bg-opacity-90 text-center inline-flex items-center justify-center gap-2"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="16"/><line x1="8" x2="16" y1="12" y2="12"/></svg> Create invoice </a> </div> <div class="bg-white rounded-lg shadow-sm border p-6"> <div class="grid grid-cols-1 md:grid-cols-3 gap-y-6 md:gap-x-6"> <div class="space-y-1"> <p class="text-sm text-slate-500">Overdue</p> <p class="text-xl sm:text-2xl font-bold tracking-tight flex items-baseline gap-2"> <?php echo format_currency($stats['total_overdue'] ?? 0); ?> </p> </div> <div class="space-y-1"> <p class="text-sm text-slate-500">Due within next 30 days</p> <p class="text-xl sm:text-2xl font-bold tracking-tight flex items-baseline gap-2"> <?php echo format_currency($stats['due_next_30_days'] ?? 0); ?> </p> </div> <div class="space-y-1"> <p class="text-sm text-slate-500">Average time to get paid</p> <p class="text-xl sm:text-2xl font-bold tracking-tight flex items-baseline gap-2"> <?php echo round($stats['avg_days_to_pay'] ?? 0); ?> <span class="text-sm font-normal text-slate-500">days</span> </p> </div> </div> </div> <form id="filter-form" method="GET"> <input type="hidden" name="tab" value="<?php echo htmlspecialchars($active_tab); ?>"> <div class="space-y-4"> <div class="flex flex-col sm:flex-row flex-wrap items-center gap-4"> <select name="customer" class="w-full sm:w-auto h-10 px-3 py-2 border border-slate-300 rounded-full bg-white text-sm"> <option value="all">All customers</option> <?php foreach($customers_list as $customer): ?> <option value="<?php echo $customer['id']; ?>" <?php echo (($_GET['customer'] ?? '') == $customer['id']) ? 'selected' : ''; ?>><?php echo htmlspecialchars($customer['name']); ?></option> <?php endforeach; ?> </select> <select name="status" class="w-full sm:w-auto h-10 px-3 py-2 border border-slate-300 rounded-full bg-white text-sm"> <option value="all">All statuses</option> <?php foreach(['Paid', 'Due', 'Overdue', 'Draft', 'Partially Paid'] as $status): ?> <option value="<?php echo $status; ?>" <?php echo (($_GET['status'] ?? '') == $status) ? 'selected' : ''; ?>><?php echo $status; ?></option> <?php endforeach; ?> </select> <input type="date" name="from_date" value="<?php echo htmlspecialchars($_GET['from_date'] ?? ''); ?>" class="w-full sm:w-auto h-10 px-3 py-2 border border-slate-300 rounded-full bg-white text-sm"> <input type="date" name="to_date" value="<?php echo htmlspecialchars($_GET['to_date'] ?? ''); ?>" class="w-full sm:w-auto h-10 px-3 py-2 border border-slate-300 rounded-full bg-white text-sm"> <input type="text" name="invoice_number" value="<?php echo htmlspecialchars($_GET['invoice_number'] ?? ''); ?>" placeholder="Enter invoice #" class="w-full sm:w-auto h-10 px-3 py-2 border border-slate-300 rounded-full bg-white text-sm"> <button type="submit" class="bg-primary text-white h-10 w-10 flex items-center justify-center rounded-full"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg></button> </div> </div> </form> <div> <div class="border-b border-slate-200"> <nav class="-mb-px flex space-x-6"> <a href="?tab=unpaid" class="tab-btn py-3 px-1 border-b-2 font-medium text-sm <?php echo $active_tab === 'unpaid' ? 'tab-active' : 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'; ?>">Unpaid <span class="ml-2 bg-slate-100 text-slate-600 rounded-full px-2 py-0.5 text-xs"><?php echo $unpaid_count; ?></span></a> <a href="?tab=draft" class="tab-btn py-3 px-1 border-b-2 font-medium text-sm <?php echo $active_tab === 'draft' ? 'tab-active' : 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'; ?>">Draft <span class="ml-2 bg-slate-100 text-slate-600 rounded-full px-2 py-0.5 text-xs"><?php echo $draft_count; ?></span></a> <a href="?tab=all" class="tab-btn py-3 px-1 border-b-2 font-medium text-sm <?php echo $active_tab === 'all' ? 'tab-active' : 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'; ?>">All invoices</a> </nav> </div> <div class="bg-white rounded-lg shadow-sm border mt-4"> <table class="w-full text-sm"> <thead class="bg-slate-50"> <tr> <th class="px-6 py-3 text-left font-medium text-slate-500">Status</th> <th class="px-6 py-3 text-left font-medium text-slate-500">Due</th> <th class="px-6 py-3 text-left font-medium text-slate-500">Date</th> <th class="px-6 py-3 text-left font-medium text-slate-500">Number</th> <th class="px-6 py-3 text-left font-medium text-slate-500">Customer</th> <th class="px-6 py-3 text-right font-medium text-slate-500">Amount</th> <th class="px-6 py-3 text-right font-medium text-slate-500"></th> </tr> </thead> <tbody> <?php if (empty($invoices)): ?> <tr><td colspan="7" class="text-center p-6 text-slate-500">No invoices match your filters.</td></tr> <?php else: ?> <?php foreach ($invoices as $invoice): ?> <tr class="border-b hover:bg-slate-50"> <td class="px-6 py-4"> <?php $status_classes = ['Paid' => 'bg-green-100 text-green-800', 'Due' => 'bg-yellow-100 text-yellow-800', 'Overdue' => 'bg-red-100 text-red-800', 'Draft' => 'bg-slate-100 text-slate-800', 'Partially Paid' => 'bg-blue-100 text-blue-800']; echo '<span class="px-2 py-1 text-xs font-semibold rounded-full ' . ($status_classes[$invoice['status']] ?? '') . '">' . htmlspecialchars($invoice['status']) . '</span>'; ?> </td> <td class="px-6 py-4 whitespace-nowrap"><?php echo htmlspecialchars(format_relative_date($invoice['due_date'])); ?></td> <td class="px-6 py-4"><?php echo htmlspecialchars(date('Y-m-d', strtotime($invoice['invoice_date']))); ?></td> <td class="px-6 py-4 font-medium"><?php echo htmlspecialchars(preg_replace('/[^0-9]/', '', $invoice['id'])); ?></td> <td class="px-6 py-4"><?php echo htmlspecialchars($invoice['customer_name']); ?></td> <td class="px-6 py-4 text-right font-semibold"><?php echo format_currency($invoice['total']); ?></td> <td class="px-6 py-4 text-right relative"> <button class="dropdown-toggle p-2 rounded-full hover:bg-slate-100"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg> </button> <div class="dropdown-menu hidden absolute right-8 top-1/2 -translate-y-1/2 w-48 bg-white rounded-md shadow-lg border z-10 p-1"> <div class="px-3 py-1.5 text-xs font-semibold text-slate-500">Actions</div> <a href="invoice-details.php?id=<?php echo htmlspecialchars($invoice['id']); ?>" class="invoice-action flex items-center gap-2 w-full text-left px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50 rounded-md" data-action="view"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>View</a> <a href="invoice-edit.php?id=<?php echo htmlspecialchars($invoice['id']); ?>" class="invoice-action flex items-center gap-2 w-full text-left px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50 rounded-md" data-action="edit"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>Edit</a> <a href="#" class="invoice-action flex items-center gap-2 w-full text-left px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50 rounded-md" data-action="duplicate"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>Duplicate</a> <hr class="my-1"> <button class="invoice-action flex items-center gap-2 w-full text-left px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50 rounded-md" data-action="send" data-id="<?php echo htmlspecialchars($invoice['id']); ?>"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>Send invoice</button> <button class="invoice-action flex items-center gap-2 w-full text-left px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50 rounded-md" data-action="mark_sent" data-id="<?php echo htmlspecialchars($invoice['id']); ?>" <?php echo $invoice['status'] !== 'Draft' ? 'disabled' : ''; ?>><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" x2="19" y1="12" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>Mark as sent</button> <hr class="my-1"> <button class="invoice-action flex items-center gap-2 w-full text-left px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50 rounded-md" data-action="pdf" data-id="<?php echo htmlspecialchars($invoice['id']); ?>"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>Export as PDF</button> <button class="invoice-action flex items-center gap-2 w-full text-left px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50 rounded-md" data-action="print" data-id="<?php echo htmlspecialchars($invoice['id']); ?>"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect width="12" height="8" x="6" y="14"/></svg>Print</button> <hr class="my-1"> <button class="invoice-action flex items-center gap-2 w-full text-left px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-50 rounded-md" data-action="payment" data-id="<?php echo htmlspecialchars($invoice['id']); ?>" data-total="<?php echo htmlspecialchars($invoice['total'] - ($invoice['amount_paid'] ?? 0)); ?>" <?php echo $invoice['status'] === 'Paid' ? 'disabled' : ''; ?>><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>Record payment</button> <hr class="my-1"> <button class="invoice-action flex items-center gap-2 w-full text-left px-3 py-1.5 text-sm text-red-600 hover:bg-red-50 rounded-md" data-action="delete" data-id="<?php echo htmlspecialchars($invoice['id']); ?>"><svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>Delete</button> </div> </td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> </div> </div> </main> </div> </div> <div id="modal-container"></div> <script> document.addEventListener('DOMContentLoaded', () => { const form = document.getElementById('filter-form'); const inputs = form.querySelectorAll('select, input'); inputs.forEach(input => { input.addEventListener('change', () => { form.submit(); }); if (input.type === 'text') { input.addEventListener('keydown', (e) => { if (e.key === 'Enter') form.submit(); }); } }); const createModal = (content) => { const modal = document.createElement('div'); modal.innerHTML = content; document.getElementById('modal-container').appendChild(modal); const closeModal = () => document.getElementById('modal-container').innerHTML = ''; modal.querySelector('.modal-cancel')?.addEventListener('click', closeModal); modal.querySelector('.modal-overlay')?.addEventListener('click', closeModal); return { modal, closeModal }; } const handleApiAction = (formData, successCallback) => { fetch('api.php', { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { if (data.success) { alert(data.data.message); if (successCallback) { successCallback(data.data); } else { location.reload(); } } else { alert('Error: ' + data.message); } }) .catch(err => { console.error(err); alert('An error occurred.'); }); }; document.body.addEventListener('click', (e) => { const dropdownToggle = e.target.closest('.dropdown-toggle'); document.querySelectorAll('.dropdown-menu').forEach(menu => { if (dropdownToggle && menu.parentElement.contains(dropdownToggle)) { menu.classList.toggle('hidden'); } else if (!menu.parentElement.contains(e.target)) { menu.classList.add('hidden'); } }); if (dropdownToggle) { e.stopPropagation(); return; } const actionTarget = e.target.closest('.invoice-action'); if (actionTarget) { const action = actionTarget.dataset.action; if (action === 'view' || action === 'edit' || action === 'duplicate') { // Allow default link behavior for view/edit, show alert for duplicate if(action === 'duplicate') { e.preventDefault(); alert('Duplicate functionality is coming soon!'); } return; } e.preventDefault(); e.stopPropagation(); const id = actionTarget.dataset.id; const formData = new FormData(); formData.append('invoice_id', id); switch(action) { case 'delete': if (confirm(`Are you sure you want to delete invoice ${id}?`)) { formData.append('action', 'delete_invoice'); handleApiAction(formData); } break; case 'mark_sent': formData.append('action', 'mark_invoice_sent'); handleApiAction(formData); break; case 'payment': const amountDue = actionTarget.dataset.total; const modalHTML = `<div class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4 modal-overlay"><div class="bg-white rounded-lg p-6 w-full max-w-md" onclick="event.stopPropagation()"><h3 class="font-bold text-lg mb-4">Record Payment for ${id}</h3><form id="payment-form" class="space-y-4"><input type="hidden" name="invoice_id" value="${id}"><input type="hidden" name="action" value="record_payment"><div><label class="block text-sm">Payment Date</label><input type="date" name="payment_date" value="<?php echo date('Y-m-d'); ?>" class="w-full p-2 border rounded mt-1"></div><div><label class="block text-sm">Amount</label><input type="number" name="amount" step="0.01" value="${amountDue}" class="w-full p-2 border rounded mt-1" required></div><div><label class="block text-sm">Method</label><input type="text" name="method" class="w-full p-2 border rounded mt-1"></div><div><label class="block text-sm">Account</label><input type="text" name="account" class="w-full p-2 border rounded mt-1"></div><div class="mt-6 flex justify-end gap-2"><button type="button" class="modal-cancel px-4 py-2 rounded border font-semibold">Cancel</button><button type="submit" class="px-4 py-2 rounded bg-primary text-white font-semibold">Record Payment</button></div></form></div></div>`; const { modal, closeModal } = createModal(modalHTML); modal.querySelector('#payment-form').onsubmit = (ev) => { ev.preventDefault(); const paymentFormData = new FormData(ev.target); handleApiAction(paymentFormData, () => { closeModal(); location.reload(); }); }; break; case 'print': window.open('invoice-details.php?id=' + id + '&print=true', '_blank'); break; default: alert('Action "' + action + '" coming soon!'); break; } } }); }); </script> </body> </html>