View file File name : invoice-form.php Content :<?php // This file is now a reusable form. It will be renamed. // To create a new invoice, simply include this file without passing $invoice_data. if (!isset($is_editing)) { $is_editing = false; $invoice_data = null; // Ensure it's null for new invoices } // Re-fetch data needed for the form if not already loaded by a parent script if (!isset($customers_list) || !isset($items_list)) { try { require 'db_config.php'; $conn = new mysqli($servername, $username, $password, $dbname); if ($conn->connect_error) throw new Exception("Connection failed"); $customers_list = []; $customers_result = $conn->query("SELECT id, name, email FROM customers ORDER BY name ASC"); if ($customers_result) { while ($row = $customers_result->fetch_assoc()) $customers_list[] = $row; } $items_list = []; $items_result = $conn->query("SELECT id, name, price, description FROM items WHERE sell = 1 ORDER BY name ASC"); if ($items_result) { while ($row = $items_result->fetch_assoc()) $items_list[] = $row; } $conn->close(); } catch (Exception $e) { $customers_list = []; $items_list = []; } } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?php echo $is_editing ? 'Edit Invoice' : 'Create Invoice'; ?> - <?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&family=Lexend:wght@700&display=swap" rel="stylesheet"> <style> body { font-family: 'Inter', sans-serif; } .font-headline { font-family: 'Lexend', 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)); } .collapsible-content { display: none; } .collapsible-section.open .collapsible-content { display: block; } .collapsible-section.open .chevron { transform: rotate(180deg); } </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-6"> <form id="invoice-form" class="space-y-8 max-w-5xl mx-auto"> <input type="hidden" name="action" value="<?php echo $is_editing ? 'update_invoice' : 'create_invoice'; ?>"> <?php if ($is_editing): ?> <input type="hidden" name="invoice_id" value="<?php echo htmlspecialchars($invoice_data['invoice']['id']); ?>"> <?php endif; ?> <div class="flex items-center justify-between"> <h1 class="text-3xl font-bold tracking-tight"><?php echo $is_editing ? 'Edit Invoice #' . htmlspecialchars(preg_replace('/[^0-9]/', '', $invoice_data['invoice']['id'])) : 'New Invoice'; ?></h1> <div class="flex items-center gap-2"> <button type="button" class="px-4 py-2 border rounded-full text-sm font-medium hover:bg-slate-100">Preview</button> <button type="submit" class="px-4 py-2 bg-primary text-white rounded-full text-sm font-semibold hover:bg-opacity-90 flex items-center gap-2"> <?php echo $is_editing ? 'Save Changes' : 'Save and continue'; ?> <?php if (!$is_editing): ?> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg> <?php endif; ?> </button> </div> </div> <div class="bg-white border rounded-lg shadow-sm"> <div class="p-4 collapsible-section open"> <button type="button" class="collapsible-trigger flex items-center justify-between w-full font-semibold"> <span>Business address and contact details, title, summary, and logo</span> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="chevron transition-transform"><polyline points="6 9 12 15 18 9"></polyline></svg> </button> <div class="collapsible-content mt-4"> <div class="flex justify-between items-start p-4"> <div class="flex flex-col items-center gap-2"> <?php if (!empty($business['logo'])): ?> <img src="<?php echo htmlspecialchars($business['logo']); ?>" alt="Logo" class="w-36 h-auto"> <?php else: ?> <div class="w-36 h-20 bg-slate-100 flex items-center justify-center rounded text-slate-400">Your Logo</div> <?php endif; ?> <a href="settings.php" class="text-primary text-sm font-medium hover:underline">Change logo</a> </div> <div class="flex-grow mx-8 space-y-4"> <input type="text" name="title" value="Invoice" class="w-full h-12 p-2 border rounded-md text-2xl"> <input type="text" name="summary" placeholder="Summary (e.g. project name)" class="w-full h-10 p-2 border rounded-md"> </div> <div class="text-right text-sm"> <p class="font-bold"><?php echo htmlspecialchars($business['name']); ?></p> <p><?php echo htmlspecialchars($business['address'] ?? ''); ?></p> <p><?php echo htmlspecialchars($business['city_state_zip'] ?? ''); ?>, <?php echo htmlspecialchars($business['country'] ?? ''); ?></p> <p><?php echo htmlspecialchars($business['phone'] ?? ''); ?></p> </div> </div> </div> </div> </div> <div class="bg-white rounded-lg shadow-sm border p-6"> <div class="grid sm:grid-cols-2 gap-6"> <div> <label class="block text-sm font-medium mb-1">Bill to</label> <select name="customer_id" required class="w-full h-10 px-3 py-2 border border-slate-300 rounded-md"> <option value="">Select a customer</option> <?php foreach ($customers_list as $customer): ?> <option value="<?php echo htmlspecialchars($customer['id']); ?>" <?php echo ($is_editing && $customer['id'] === $invoice_data['invoice']['customer_id']) ? 'selected' : ''; ?>><?php echo htmlspecialchars($customer['name']); ?></option> <?php endforeach; ?> </select> </div> <div class="space-y-4"> <div class="flex items-center justify-between"><label class="text-sm font-medium">Invoice number</label><input type="text" name="invoice_id_display" value="<?php echo htmlspecialchars($is_editing ? $invoice_data['invoice']['id'] : 'INV-' . time()); ?>" readonly class="w-1/2 h-10 p-2 border rounded-md bg-slate-100"></div> <input type="hidden" name="invoice_id" value="<?php echo htmlspecialchars($is_editing ? $invoice_data['invoice']['id'] : 'INV-' . time()); ?>"> <div class="flex items-center justify-between"><label class="text-sm font-medium">P.O./S.O. number</label><input type="text" name="po_number" class="w-1/2 h-10 p-2 border rounded-md" value="<?php echo htmlspecialchars($invoice_data['invoice']['po_number'] ?? ''); ?>"></div> <div class="flex items-center justify-between"><label class="text-sm font-medium">Invoice Date</label><input type="date" name="invoice_date" value="<?php echo date('Y-m-d', strtotime($invoice_data['invoice']['invoice_date'] ?? 'now')); ?>" required class="w-1/2 h-10 p-2 border rounded-md"></div> <div class="flex items-center justify-between"><label class="text-sm font-medium">Payment due</label><input type="date" name="due_date" value="<?php echo date('Y-m-d', strtotime($invoice_data['invoice']['due_date'] ?? '+30 days')); ?>" required class="w-1/2 h-10 p-2 border rounded-md"></div> </div> </div> </div> <div class="bg-white rounded-lg shadow-sm border"> <table class="w-full"> <thead class="bg-slate-50"> <tr> <th class="p-3 w-8"></th> <th class="p-3 text-left text-sm font-semibold text-slate-600">Items</th> <th class="p-3 w-32 text-left text-sm font-semibold text-slate-600">Quantity</th> <th class="p-3 w-40 text-right text-sm font-semibold text-slate-600">Price</th> <th class="p-3 w-40 text-right text-sm font-semibold text-slate-600">Amount</th> <th class="p-3 w-12"></th> </tr> </thead> <tbody id="line-items-container"></tbody> </table> <div class="p-4"> <button type="button" id="add-item-btn" class="px-4 py-2 border rounded-full text-sm font-medium hover:bg-slate-50 flex items-center gap-2"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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> Add an item </button> </div> <div class="flex flex-col items-end gap-4 p-6 bg-slate-50/50"> <div class="w-full max-w-sm space-y-2"> <div class="flex justify-between"><span>Subtotal</span><span id="subtotal">BZ$0.00</span></div> <div class="flex justify-between items-center"><span>Discount</span><input type="number" name="discount" id="discount-input" value="<?php echo htmlspecialchars($invoice_data['invoice']['discount'] ?? '0.00'); ?>" class="w-24 p-2 text-right border rounded-md"></div> <div class="flex justify-between items-center"> <span>Tax</span> <div class="flex items-center gap-1"> <input type="number" name="tax" id="tax-input" value="<?php echo htmlspecialchars($invoice_data['invoice']['tax'] ?? '12.5'); ?>" step="0.01" class="w-20 p-2 text-right border rounded-md"> <span>%</span> </div> </div> <div class="flex justify-between"><span>Tax Amount</span><span id="tax-amount">BZ$0.00</span></div> <hr> <div class="flex justify-between font-bold text-lg"><span>Amount Due</span><span id="total-amount">BZ$0.00</span></div> </div> </div> </div> <div class="bg-white border rounded-lg shadow-sm"> <div class="p-4 collapsible-section"> <button type="button" class="collapsible-trigger flex items-center justify-between w-full font-semibold"> <span>Notes / Terms</span> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="chevron transition-transform"><polyline points="6 9 12 15 18 9"></polyline></svg> </button> <div class="collapsible-content mt-4"> <textarea name="notes" class="w-full p-2 border rounded-md" rows="3" placeholder="Enter notes or terms of service..."><?php echo htmlspecialchars($invoice_data['invoice']['notes'] ?? ''); ?></textarea> </div> </div> <hr> <div class="p-4 collapsible-section"> <button type="button" class="collapsible-trigger flex items-center justify-between w-full font-semibold"> <span>Footer</span> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="chevron transition-transform"><polyline points="6 9 12 15 18 9"></polyline></svg> </button> <div class="collapsible-content mt-4"> <textarea name="footer" class="w-full p-2 border rounded-md" rows="2"><?php echo htmlspecialchars($invoice_data['invoice']['footer'] ?? 'Payment can be made to Account Number: 211870867 (Atlantic Bank) - Virgilio Cus'); ?></textarea> </div> </div> </div> <div class="mt-8 flex justify-end gap-2"> <button type="button" class="px-4 py-2 border rounded-full text-sm font-medium hover:bg-slate-100">Preview</button> <button type="submit" class="px-4 py-2 bg-primary text-white rounded-full text-sm font-semibold hover:bg-opacity-90 flex items-center gap-2"> <?php echo $is_editing ? 'Save Changes' : 'Save and continue'; ?> <?php if (!$is_editing): ?> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg> <?php endif; ?> </button> </div> </form> </main> </div> </div> <script> document.addEventListener('DOMContentLoaded', () => { const itemsContainer = document.getElementById('line-items-container'); const itemsData = <?php echo json_encode($items_list); ?>; let lineItems = <?php echo $is_editing ? json_encode($invoice_data['items']) : '[]'; ?>; const addLineItem = (item = { item_id: '', description: '', quantity: 1, price: 0 }) => { lineItems.push(item); renderLineItems(); }; const renderLineItems = () => { itemsContainer.innerHTML = ''; if (lineItems.length === 0) { const placeholderRow = document.createElement('tr'); placeholderRow.innerHTML = `<td colspan="6" class="text-center text-slate-400 p-8">No items have been added yet.</td>`; itemsContainer.appendChild(placeholderRow); } else { lineItems.forEach((item, index) => { const itemRow = document.createElement('tr'); itemRow.className = 'line-item-row border-b'; itemRow.innerHTML = ` <td class="p-3 align-top"><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" class="text-slate-400 cursor-grab"><circle cx="12" cy="9" r="1"/><circle cx="12" cy="15" r="1"/><circle cx="12" cy="3" r="1"/><circle cx="12" cy="21" r="1"/></svg></td> <td class="p-3 align-top"> <select class="item-select w-full h-10 p-2 border rounded-md" data-index="${index}"> <option value="">Select an item</option> ${itemsData.map(d => `<option value="${d.id}" ${d.id === item.item_id ? 'selected' : ''} data-price="${d.price}" data-description="${d.description}">${d.name}</option>`).join('')} </select> <textarea class="item-description w-full p-2 mt-1 border rounded-md text-sm text-slate-500" rows="1" placeholder="Item description" data-index="${index}">${item.description || ''}</textarea> </td> <td class="p-3 align-top"><input type="number" value="${item.quantity}" min="1" class="item-quantity w-full h-10 p-2 border rounded-md" data-index="${index}"></td> <td class="p-3 align-top"><input type="number" value="${parseFloat(item.price).toFixed(2)}" step="0.01" class="item-price w-full h-10 p-2 text-right border rounded-md" data-index="${index}"></td> <td class="p-3 align-top text-right font-medium item-amount">${formatCurrency(item.quantity * item.price)}</td> <td class="p-3 align-top"> <button type="button" class="remove-item-btn text-slate-400 hover:text-red-600" data-index="${index}"> <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"><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> </button> </td> `; itemsContainer.appendChild(itemRow); }); } calculateTotals(); }; const formatCurrency = (amount) => `BZ$${parseFloat(amount).toFixed(2)}`; const calculateTotals = () => { let subtotal = 0; lineItems.forEach(item => { subtotal += (item.quantity || 0) * (item.price || 0); }); const discount = parseFloat(document.getElementById('discount-input').value) || 0; const taxRate = parseFloat(document.getElementById('tax-input').value) || 0; const taxAmount = (subtotal - discount) * (taxRate / 100); const total = subtotal - discount + taxAmount; document.getElementById('subtotal').textContent = formatCurrency(subtotal); document.getElementById('tax-amount').textContent = formatCurrency(taxAmount); document.getElementById('total-amount').textContent = formatCurrency(total); }; document.getElementById('add-item-btn').addEventListener('click', () => addLineItem()); document.getElementById('discount-input').addEventListener('input', calculateTotals); document.getElementById('tax-input').addEventListener('input', calculateTotals); itemsContainer.addEventListener('change', (e) => { const target = e.target; const index = parseInt(target.dataset.index); if (target.classList.contains('item-select')) { const selectedOption = target.options[target.selectedIndex]; lineItems[index].item_id = target.value; // Use item_id to match db schema lineItems[index].price = parseFloat(selectedOption.dataset.price || '0.00'); lineItems[index].description = selectedOption.dataset.description || ''; renderLineItems(); } }); itemsContainer.addEventListener('input', (e) => { const target = e.target; const index = parseInt(target.dataset.index); if (target.classList.contains('item-quantity')) { lineItems[index].quantity = parseInt(target.value) || 1; } if (target.classList.contains('item-price')) { lineItems[index].price = parseFloat(target.value) || 0; } if (target.classList.contains('item-description')) { lineItems[index].description = target.value; } calculateTotals(); // Update the amount in the currently edited row const row = target.closest('.line-item-row'); if(row) { row.querySelector('.item-amount').textContent = formatCurrency(lineItems[index].quantity * lineItems[index].price); } }); itemsContainer.addEventListener('click', (e) => { const removeBtn = e.target.closest('.remove-item-btn'); if (removeBtn) { const index = parseInt(removeBtn.dataset.index); lineItems.splice(index, 1); renderLineItems(); } }); document.querySelectorAll('.collapsible-trigger').forEach(trigger => { trigger.addEventListener('click', () => { trigger.closest('.collapsible-section').classList.toggle('open'); }); }); if (lineItems.length === 0) { addLineItem(); } else { renderLineItems(); } document.getElementById('invoice-form').addEventListener('submit', function(e) { e.preventDefault(); const formData = new FormData(this); formData.append('items', JSON.stringify(lineItems)); const action = this.querySelector('input[name="action"]').value; const successMessage = action === 'update_invoice' ? 'Invoice updated successfully!' : 'Invoice created successfully!'; fetch('api.php', { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { if (data.success) { alert(successMessage); window.location.href = 'invoice-details.php?id=' + data.data.invoice_id; } else { alert('Error: ' + data.message); } }).catch(err => { console.error(err); alert('An error occurred. Please try again.'); }); }); }); </script> </body> </html>