|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>SmartBill - Invoice Generator</title> |
|
|
<link rel="icon" type="image/x-icon" href="/static/favicon.ico"> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet"> |
|
|
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> |
|
|
<script src="https://unpkg.com/feather-icons"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> |
|
|
<style> |
|
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); |
|
|
body { |
|
|
font-family: 'Poppins', sans-serif; |
|
|
} |
|
|
.invoice-paper { |
|
|
background: white; |
|
|
box-shadow: 0 0 10px rgba(0,0,0,0.1); |
|
|
} |
|
|
.divider { |
|
|
border-bottom: 1px dashed #e2e8f0; |
|
|
} |
|
|
.print-only { |
|
|
display: none; |
|
|
} |
|
|
@media print { |
|
|
.no-print { |
|
|
display: none !important; |
|
|
} |
|
|
.print-only { |
|
|
display: block !important; |
|
|
} |
|
|
body { |
|
|
background: white !important; |
|
|
} |
|
|
.invoice-paper { |
|
|
box-shadow: none !important; |
|
|
margin: 0 !important; |
|
|
padding: 0 !important; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-gray-50"> |
|
|
<div class="min-h-screen"> |
|
|
|
|
|
<header class="bg-indigo-600 text-white shadow-lg no-print"> |
|
|
<div class="container mx-auto px-4 py-4 flex justify-between items-center"> |
|
|
<div class="flex items-center space-x-2"> |
|
|
<i data-feather="file-text" class="w-8 h-8"></i> |
|
|
<h1 class="text-2xl font-bold">SmartBill</h1> |
|
|
</div> |
|
|
<nav> |
|
|
<ul class="flex space-x-6"> |
|
|
<li><a href="#" class="hover:text-indigo-200 transition">Dashboard</a></li> |
|
|
<li><a href="#bills" class="hover:text-indigo-200 transition">Bills</a></li> |
|
|
<li><a href="#customers" class="hover:text-indigo-200 transition">Customers</a></li> |
|
|
</ul> |
|
|
</nav> |
|
|
<button class="bg-white text-indigo-600 px-4 py-2 rounded-lg font-medium hover:bg-indigo-50 transition"> |
|
|
<i data-feather="user" class="inline mr-2"></i> Account |
|
|
</button> |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
|
|
|
<main class="container mx-auto px-4 py-8"> |
|
|
<div class="flex flex-col lg:flex-row gap-8"> |
|
|
|
|
|
<div class="lg:w-2/3 no-print" data-aos="fade-right"> |
|
|
<div class="bg-white rounded-xl shadow-md p-6 mb-6"> |
|
|
<h2 class="text-xl font-bold text-gray-800 mb-4">Create New Bill</h2> |
|
|
|
|
|
|
|
|
<div class="mb-6"> |
|
|
<h3 class="text-lg font-semibold text-gray-700 mb-3">Customer Information</h3> |
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Name</label> |
|
|
<input type="text" id="customer-name" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Phone</label> |
|
|
<input type="tel" id="customer-phone" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> |
|
|
</div> |
|
|
<div class="md:col-span-2"> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Address</label> |
|
|
<textarea id="customer-address" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"></textarea> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="mb-6"> |
|
|
<div class="flex justify-between items-center mb-3"> |
|
|
<h3 class="text-lg font-semibold text-gray-700">Items</h3> |
|
|
<button id="add-item" class="bg-indigo-100 text-indigo-600 px-3 py-1 rounded-lg text-sm font-medium hover:bg-indigo-200 transition"> |
|
|
<i data-feather="plus" class="w-4 h-4 inline mr-1"></i> Add Item |
|
|
</button> |
|
|
</div> |
|
|
<div class="overflow-x-auto"> |
|
|
<table class="min-w-full divide-y divide-gray-200"> |
|
|
<thead class="bg-gray-50"> |
|
|
<tr> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Item</th> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Quantity</th> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Price</th> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total</th> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody id="items-table" class="bg-white divide-y divide-gray-200"> |
|
|
|
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="mb-6"> |
|
|
<h3 class="text-lg font-semibold text-gray-700 mb-3">Payment Information</h3> |
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Subtotal</label> |
|
|
<input type="text" id="subtotal" readonly class="w-full px-4 py-2 bg-gray-100 border border-gray-300 rounded-lg"> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Tax (%)</label> |
|
|
<input type="number" id="tax" value="0" min="0" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Discount</label> |
|
|
<input type="number" id="discount" value="0" min="0" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> |
|
|
</div> |
|
|
<div class="md:col-span-3"> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Total Amount</label> |
|
|
<input type="text" id="total-amount" readonly class="w-full px-4 py-3 bg-gray-100 border border-gray-300 rounded-lg text-xl font-bold"> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="flex justify-end space-x-4"> |
|
|
<button id="clear-form" class="px-6 py-2 border border-gray-300 rounded-lg font-medium text-gray-700 hover:bg-gray-50 transition"> |
|
|
Clear |
|
|
</button> |
|
|
<button id="save-bill" class="px-6 py-2 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700 transition"> |
|
|
Save Bill |
|
|
</button> |
|
|
<button id="generate-bill" class="px-6 py-2 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 transition"> |
|
|
Generate Bill |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="lg:w-1/3 no-print" data-aos="fade-left"> |
|
|
<div class="sticky top-4"> |
|
|
<div class="bg-white rounded-xl shadow-md p-6 mb-6"> |
|
|
<h2 class="text-xl font-bold text-gray-800 mb-4">Bill Preview</h2> |
|
|
<div id="invoice-preview" class="invoice-paper p-6"> |
|
|
|
|
|
<div class="text-center py-12 text-gray-400"> |
|
|
<i data-feather="file-text" class="w-12 h-12 mx-auto mb-4"></i> |
|
|
<p>Your bill preview will appear here</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<section id="bills" class="mt-12 no-print" data-aos="fade-up"> |
|
|
<div class="bg-white rounded-xl shadow-md p-6"> |
|
|
<div class="flex justify-between items-center mb-6"> |
|
|
<h2 class="text-xl font-bold text-gray-800">Saved Bills</h2> |
|
|
<div class="relative"> |
|
|
<input type="text" placeholder="Search bills..." class="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> |
|
|
<i data-feather="search" class="absolute left-3 top-2.5 text-gray-400"></i> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="overflow-x-auto"> |
|
|
<table class="min-w-full divide-y divide-gray-200"> |
|
|
<thead class="bg-gray-50"> |
|
|
<tr> |
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Bill No.</th> |
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th> |
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Customer</th> |
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Amount</th> |
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th> |
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody id="saved-bills" class="bg-white divide-y divide-gray-200"> |
|
|
|
|
|
<tr> |
|
|
<td colspan="6" class="px-6 py-4 text-center text-gray-500">No bills saved yet</td> |
|
|
</tr> |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
</main> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="invoice-template" class="hidden"> |
|
|
<div class="invoice-paper p-8 max-w-3xl mx-auto"> |
|
|
<div class="flex justify-between items-start mb-8"> |
|
|
<div> |
|
|
<h1 class="text-3xl font-bold text-indigo-600">SMARTBILL</h1> |
|
|
<p class="text-gray-600">123 Business Street, City</p> |
|
|
<p class="text-gray-600">Phone: (123) 456-7890</p> |
|
|
<p class="text-gray-600">Email: info@smartbill.com</p> |
|
|
</div> |
|
|
<div class="text-right"> |
|
|
<h2 class="text-2xl font-bold text-gray-800">INVOICE</h2> |
|
|
<p class="text-gray-600">#<span id="invoice-number">0001</span></p> |
|
|
<p class="text-gray-600">Date: <span id="invoice-date">01 Jan 2023</span></p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8"> |
|
|
<div> |
|
|
<h3 class="text-lg font-semibold text-gray-800 mb-2">Bill To:</h3> |
|
|
<p class="font-medium" id="invoice-customer-name">Customer Name</p> |
|
|
<p id="invoice-customer-phone">Phone: </p> |
|
|
<p id="invoice-customer-address">Address: </p> |
|
|
</div> |
|
|
<div class="text-right md:text-left"> |
|
|
<h3 class="text-lg font-semibold text-gray-800 mb-2">Payment Details:</h3> |
|
|
<p>Subtotal: <span id="invoice-subtotal" class="font-medium">$0.00</span></p> |
|
|
<p>Tax (<span id="invoice-tax-rate">0</span>%): <span id="invoice-tax" class="font-medium">$0.00</span></p> |
|
|
<p>Discount: <span id="invoice-discount" class="font-medium">$0.00</span></p> |
|
|
<p class="text-xl font-bold mt-2">Total: <span id="invoice-total">$0.00</span></p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="mb-8"> |
|
|
<table class="min-w-full"> |
|
|
<thead> |
|
|
<tr class="border-b border-gray-200"> |
|
|
<th class="text-left py-2 font-medium text-gray-700">Item</th> |
|
|
<th class="text-right py-2 font-medium text-gray-700">Qty</th> |
|
|
<th class="text-right py-2 font-medium text-gray-700">Price</th> |
|
|
<th class="text-right py-2 font-medium text-gray-700">Total</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody id="invoice-items"> |
|
|
|
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
|
|
|
<div class="border-t border-gray-200 pt-6"> |
|
|
<p class="text-gray-600 text-sm">Thank you for your business!</p> |
|
|
<div class="print-only mt-8 pt-6 border-t border-gray-200"> |
|
|
<p class="text-sm text-gray-500">This is a computer generated invoice. No signature required.</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
AOS.init(); |
|
|
feather.replace(); |
|
|
|
|
|
|
|
|
let items = []; |
|
|
let savedBills = JSON.parse(localStorage.getItem('savedBills')) || []; |
|
|
let currentBillNumber = parseInt(localStorage.getItem('lastBillNumber')) || 1; |
|
|
|
|
|
|
|
|
const itemsTable = document.getElementById('items-table'); |
|
|
const addItemBtn = document.getElementById('add-item'); |
|
|
const saveBillBtn = document.getElementById('save-bill'); |
|
|
const generateBillBtn = document.getElementById('generate-bill'); |
|
|
const clearFormBtn = document.getElementById('clear-form'); |
|
|
const subtotalInput = document.getElementById('subtotal'); |
|
|
const taxInput = document.getElementById('tax'); |
|
|
const discountInput = document.getElementById('discount'); |
|
|
const totalAmountInput = document.getElementById('total-amount'); |
|
|
const invoicePreview = document.getElementById('invoice-preview'); |
|
|
const savedBillsTable = document.getElementById('saved-bills'); |
|
|
|
|
|
|
|
|
addItemBtn.addEventListener('click', function() { |
|
|
const itemId = Date.now(); |
|
|
items.push({ |
|
|
id: itemId, |
|
|
name: '', |
|
|
quantity: 1, |
|
|
price: 0, |
|
|
total: 0 |
|
|
}); |
|
|
|
|
|
renderItemsTable(); |
|
|
updateCalculations(); |
|
|
}); |
|
|
|
|
|
|
|
|
function renderItemsTable() { |
|
|
itemsTable.innerHTML = ''; |
|
|
|
|
|
if (items.length === 0) { |
|
|
itemsTable.innerHTML = ` |
|
|
<tr> |
|
|
<td colspan="5" class="px-4 py-4 text-center text-gray-500">No items added yet</td> |
|
|
</tr> |
|
|
`; |
|
|
return; |
|
|
} |
|
|
|
|
|
items.forEach((item, index) => { |
|
|
const row = document.createElement('tr'); |
|
|
row.className = index % 2 === 0 ? 'bg-white' : 'bg-gray-50'; |
|
|
row.innerHTML = ` |
|
|
<td class="px-4 py-2 whitespace-nowrap"> |
|
|
<input type="text" value="${item.name}" data-id="${item.id}" data-field="name" class="w-full px-2 py-1 border border-gray-300 rounded focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"> |
|
|
</td> |
|
|
<td class="px-4 py-2 whitespace-nowrap"> |
|
|
<input type="number" value="${item.quantity}" min="1" data-id="${item.id}" data-field="quantity" class="w-full px-2 py-1 border border-gray-300 rounded focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"> |
|
|
</td> |
|
|
<td class="px-4 py-2 whitespace-nowrap"> |
|
|
<input type="number" value="${item.price}" min="0" step="0.01" data-id="${item.id}" data-field="price" class="w-full px-2 py-1 border border-gray-300 rounded focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"> |
|
|
</td> |
|
|
<td class="px-4 py-2 whitespace-nowrap font-medium">$${item.total.toFixed(2)}</td> |
|
|
<td class="px-4 py-2 whitespace-nowrap"> |
|
|
<button data-id="${item.id}" class="delete-item text-red-600 hover:text-red-800"> |
|
|
<i data-feather="trash-2" class="w-4 h-4"></i> |
|
|
</button> |
|
|
</td> |
|
|
`; |
|
|
itemsTable.appendChild(row); |
|
|
|
|
|
|
|
|
const inputs = row.querySelectorAll('input'); |
|
|
inputs.forEach(input => { |
|
|
input.addEventListener('change', function() { |
|
|
const id = parseInt(this.dataset.id); |
|
|
const field = this.dataset.field; |
|
|
const value = field === 'name' ? this.value : parseFloat(this.value); |
|
|
|
|
|
updateItem(id, field, value); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const deleteBtn = row.querySelector('.delete-item'); |
|
|
deleteBtn.addEventListener('click', function() { |
|
|
const id = parseInt(this.dataset.id); |
|
|
deleteItem(id); |
|
|
}); |
|
|
}); |
|
|
|
|
|
feather.replace(); |
|
|
} |
|
|
|
|
|
|
|
|
function updateItem(id, field, value) { |
|
|
const itemIndex = items.findIndex(item => item.id === id); |
|
|
if (itemIndex !== -1) { |
|
|
items[itemIndex][field] = value; |
|
|
|
|
|
|
|
|
if (field === 'quantity' || field === 'price') { |
|
|
items[itemIndex].total = items[itemIndex].quantity * items[itemIndex].price; |
|
|
} |
|
|
|
|
|
renderItemsTable(); |
|
|
updateCalculations(); |
|
|
updateInvoicePreview(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function deleteItem(id) { |
|
|
items = items.filter(item => item.id !== id); |
|
|
renderItemsTable(); |
|
|
updateCalculations(); |
|
|
updateInvoicePreview(); |
|
|
} |
|
|
|
|
|
|
|
|
function updateCalculations() { |
|
|
const subtotal = items.reduce((sum, item) => sum + item.total, 0); |
|
|
const taxRate = parseFloat(taxInput.value) || 0; |
|
|
const discountAmount = parseFloat(discountInput.value) || 0; |
|
|
const taxAmount = subtotal * (taxRate / 100); |
|
|
const total = subtotal + taxAmount - discountAmount; |
|
|
|
|
|
subtotalInput.value = subtotal.toFixed(2); |
|
|
totalAmountInput.value = total.toFixed(2); |
|
|
} |
|
|
|
|
|
|
|
|
function updateInvoicePreview() { |
|
|
const customerName = document.getElementById('customer-name').value || 'Customer Name'; |
|
|
const customerPhone = document.getElementById('customer-phone').value || ''; |
|
|
const customerAddress = document.getElementById('customer-address').value || ''; |
|
|
const subtotal = parseFloat(subtotalInput.value) || 0; |
|
|
const taxRate = parseFloat(taxInput.value) || 0; |
|
|
const discountAmount = parseFloat(discountInput.value) || 0; |
|
|
const taxAmount = subtotal * (taxRate / 100); |
|
|
const total = subtotal + taxAmount - discountAmount; |
|
|
|
|
|
|
|
|
const today = new Date(); |
|
|
const options = { year: 'numeric', month: 'short', day: 'numeric' }; |
|
|
const formattedDate = today.toLocaleDateString('en-US', options); |
|
|
|
|
|
|
|
|
let invoiceHTML = document.getElementById('invoice-template').innerHTML; |
|
|
|
|
|
|
|
|
invoiceHTML = invoiceHTML |
|
|
.replace('0001', currentBillNumber.toString().padStart(4, '0')) |
|
|
.replace('01 Jan 2023', formattedDate) |
|
|
.replace('Customer Name', customerName) |
|
|
.replace('Phone: ', customerPhone ? `Phone: ${customerPhone}` : '') |
|
|
.replace('Address: ', customerAddress ? `Address: ${customerAddress}` : '') |
|
|
.replace('$0.00', `$${subtotal.toFixed(2)}`) |
|
|
.replace('0', taxRate.toString()) |
|
|
.replace('$0.00', `$${taxAmount.toFixed(2)}`) |
|
|
.replace('$0.00', `$${discountAmount.toFixed(2)}`) |
|
|
.replace('$0.00', `$${total.toFixed(2)}`); |
|
|
|
|
|
|
|
|
const itemsStart = invoiceHTML.indexOf('<tbody id="invoice-items">') + 26; |
|
|
const itemsEnd = invoiceHTML.indexOf('</tbody>', itemsStart); |
|
|
|
|
|
let itemsHTML = ''; |
|
|
items.forEach(item => { |
|
|
itemsHTML += ` |
|
|
<tr class="border-b border-gray-100"> |
|
|
<td class="py-2">${item.name || 'Item'}</td> |
|
|
<td class="py-2 text-right">${item.quantity}</td> |
|
|
<td class="py-2 text-right">$${item.price.toFixed(2)}</td> |
|
|
<td class="py-2 text-right">$${item.total.toFixed(2)}</td> |
|
|
</tr> |
|
|
`; |
|
|
}); |
|
|
|
|
|
invoiceHTML = invoiceHTML.slice(0, itemsStart) + itemsHTML + invoiceHTML.slice(itemsEnd); |
|
|
|
|
|
|
|
|
invoicePreview.innerHTML = invoiceHTML; |
|
|
feather.replace(); |
|
|
} |
|
|
|
|
|
|
|
|
saveBillBtn.addEventListener('click', function() { |
|
|
if (items.length === 0) { |
|
|
alert('Please add at least one item to the bill'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const customerName = document.getElementById('customer-name').value || 'Customer'; |
|
|
const subtotal = parseFloat(subtotalInput.value) || 0; |
|
|
const total = parseFloat(totalAmountInput.value) || 0; |
|
|
const today = new Date(); |
|
|
|
|
|
const bill = { |
|
|
id: Date.now(), |
|
|
number: currentBillNumber, |
|
|
date: today.toISOString(), |
|
|
customer: customerName, |
|
|
items: [...items], |
|
|
subtotal: subtotal, |
|
|
tax: parseFloat(taxInput.value) || 0, |
|
|
discount: parseFloat(discountInput.value) || 0, |
|
|
total: total, |
|
|
status: 'unpaid' |
|
|
}; |
|
|
|
|
|
savedBills.unshift(bill); |
|
|
localStorage.setItem('savedBills', JSON.stringify(savedBills)); |
|
|
currentBillNumber++; |
|
|
localStorage.setItem('lastBillNumber', currentBillNumber.toString()); |
|
|
|
|
|
renderSavedBills(); |
|
|
alert('Bill saved successfully!'); |
|
|
}); |
|
|
|
|
|
|
|
|
generateBillBtn.addEventListener('click', function() { |
|
|
if (items.length === 0) { |
|
|
alert('Please add at least one item to the bill'); |
|
|
return; |
|
|
} |
|
|
|
|
|
updateInvoicePreview(); |
|
|
|
|
|
|
|
|
const invoiceClone = invoicePreview.cloneNode(true); |
|
|
invoiceClone.classList.remove('no-print'); |
|
|
|
|
|
|
|
|
const printWindow = window.open('', '_blank'); |
|
|
printWindow.document.write(` |
|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<title>Invoice #${currentBillNumber.toString().padStart(4, '0')}</title> |
|
|
<style> |
|
|
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; } |
|
|
.invoice-paper { max-width: 800px; margin: 0 auto; background: white; padding: 20px; } |
|
|
table { width: 100%; border-collapse: collapse; } |
|
|
th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; } |
|
|
.text-right { text-align: right; } |
|
|
.font-bold { font-weight: bold; } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
`); |
|
|
printWindow.document.write(invoiceClone.innerHTML); |
|
|
printWindow.document.write('</body></html>'); |
|
|
printWindow.document.close(); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
printWindow.print(); |
|
|
}, 500); |
|
|
}); |
|
|
|
|
|
|
|
|
clearFormBtn.addEventListener('click', function() { |
|
|
if (confirm('Are you sure you want to clear the current bill?')) { |
|
|
items = []; |
|
|
document.getElementById('customer-name').value = ''; |
|
|
document.getElementById('customer-phone').value = ''; |
|
|
document.getElementById('customer-address').value = ''; |
|
|
taxInput.value = '0'; |
|
|
discountInput.value = '0'; |
|
|
|
|
|
renderItemsTable(); |
|
|
updateCalculations(); |
|
|
updateInvoicePreview(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
function renderSavedBills() { |
|
|
savedBillsTable.innerHTML = ''; |
|
|
|
|
|
if (savedBills.length === 0) { |
|
|
savedBillsTable.innerHTML = ` |
|
|
<tr> |
|
|
<td colspan="6" class="px-6 py-4 text-center text-gray-500">No bills saved yet</td> |
|
|
</tr> |
|
|
`; |
|
|
return; |
|
|
} |
|
|
|
|
|
savedBills.forEach(bill => { |
|
|
const date = new Date(bill.date); |
|
|
const formattedDate = date.toLocaleDateString('en-US', { |
|
|
year: 'numeric', |
|
|
month: 'short', |
|
|
day: 'numeric' |
|
|
}); |
|
|
|
|
|
const row = document.createElement('tr'); |
|
|
row.className = 'hover:bg-gray-50'; |
|
|
row.innerHTML = ` |
|
|
<td class="px-6 py-4 whitespace-nowrap">${bill.number.toString().padStart(4, '0')}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap">${formattedDate}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap">${bill.customer}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap">$${bill.total.toFixed(2)}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap"> |
|
|
<span class="px-2 py-1 text-xs rounded-full ${bill.status === 'paid' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}"> |
|
|
${bill.status} |
|
|
</span> |
|
|
</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> |
|
|
<button data-id="${bill.id}" class="view-bill text-indigo-600 hover:text-indigo-900 mr-3"> |
|
|
<i data-feather="eye" class="w-4 h-4"></i> |
|
|
</button> |
|
|
<button data-id="${bill.id}" class="print-bill text-gray-600 hover:text-gray-900 mr-3"> |
|
|
<i data-feather="printer" class="w-4 h-4"></i> |
|
|
</button> |
|
|
<button data-id="${bill.id}" class="delete-bill text-red-600 hover:text-red-900"> |
|
|
<i data-feather="trash-2" class="w-4 h-4"></i> |
|
|
</button> |
|
|
</td> |
|
|
`; |
|
|
savedBillsTable.appendChild(row); |
|
|
|
|
|
|
|
|
const viewBtn = row.querySelector('.view-bill'); |
|
|
const printBtn = row.querySelector('.print-bill'); |
|
|
const deleteBtn = row.querySelector('.delete-bill'); |
|
|
|
|
|
viewBtn.addEventListener('click', () => viewBill(bill.id)); |
|
|
printBtn.addEventListener('click', () => printBill(bill.id)); |
|
|
deleteBtn.addEventListener('click', () => deleteSavedBill(bill.id)); |
|
|
}); |
|
|
|
|
|
feather.replace(); |
|
|
} |
|
|
|
|
|
|
|
|
function viewBill(id) { |
|
|
const bill = savedBills.find(b => b.id === id); |
|
|
if (!bill) return; |
|
|
|
|
|
|
|
|
items = []; |
|
|
document.getElementById('customer-name').value = bill.customer; |
|
|
taxInput.value = bill.tax; |
|
|
discountInput.value = bill.discount; |
|
|
|
|
|
|
|
|
bill.items.forEach(item => { |
|
|
items.push({...item}); |
|
|
}); |
|
|
|
|
|
renderItemsTable(); |
|
|
updateCalculations(); |
|
|
updateInvoicePreview(); |
|
|
|
|
|
|
|
|
document.querySelector('main').scrollIntoView({ behavior: 'smooth' }); |
|
|
} |
|
|
|
|
|
|
|
|
function printBill(id) { |
|
|
const bill = savedBills.find(b => b.id === id); |
|
|
if (!bill) return; |
|
|
|
|
|
|
|
|
const date = new Date(bill.date); |
|
|
const formattedDate = date.toLocaleDateString('en-US', { |
|
|
year: 'numeric', |
|
|
month: 'short', |
|
|
day: 'numeric' |
|
|
}); |
|
|
|
|
|
|
|
|
let invoiceHTML = document.getElementById('invoice-template').innerHTML; |
|
|
|
|
|
|
|
|
invoiceHTML = invoiceHTML |
|
|
.replace('0001', bill.number.toString().padStart(4, '0')) |
|
|
.replace('01 Jan 2023', formattedDate) |
|
|
.replace('Customer Name', bill.customer) |
|
|
.replace('$0.00', `$${bill.subtotal.toFixed(2)}`) |
|
|
.replace('0', bill.tax.toString()) |
|
|
.replace('$0.00', `$${(bill.subtotal * (bill.tax / 100)).toFixed(2)}`) |
|
|
.replace('$0.00', `$${bill.discount.toFixed(2)}`) |
|
|
.replace('$0.00', `$${bill.total.toFixed(2)}`); |
|
|
|
|
|
|
|
|
const itemsStart = invoiceHTML.indexOf('<tbody id="invoice-items">') + 26; |
|
|
const itemsEnd = invoiceHTML.indexOf('</tbody>', itemsStart); |
|
|
|
|
|
let itemsHTML = ''; |
|
|
bill.items.forEach(item => { |
|
|
itemsHTML += ` |
|
|
<tr class="border-b border-gray-100"> |
|
|
<td class="py-2">${item.name || 'Item'}</td> |
|
|
<td class="py-2 text-right">${item.quantity}</td> |
|
|
<td class="py-2 text-right">$${item.price.toFixed(2)}</td> |
|
|
<td class="py-2 text-right">$${item.total.toFixed(2)}</td> |
|
|
</tr> |
|
|
`; |
|
|
}); |
|
|
|
|
|
invoiceHTML = invoiceHTML.slice(0, itemsStart) + itemsHTML + invoiceHTML.slice(itemsEnd); |
|
|
|
|
|
|
|
|
const printWindow = window.open('', '_blank'); |
|
|
printWindow.document.write(` |
|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<title>Invoice #${bill.number.toString().padStart(4, '0')}</title> |
|
|
<style> |
|
|
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; } |
|
|
.invoice-paper { max-width: 800px; margin: 0 auto; background: white; padding: 20px; } |
|
|
table { width: 100%; border-collapse: collapse; } |
|
|
th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; } |
|
|
.text-right { text-align: right; } |
|
|
.font-bold { font-weight: bold; } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
${invoiceHTML} |
|
|
</body> |
|
|
</html> |
|
|
`); |
|
|
printWindow.document.close(); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
printWindow.print(); |
|
|
}, 500); |
|
|
} |
|
|
|
|
|
|
|
|
function deleteSavedBill(id) { |
|
|
if (confirm('Are you sure you want to delete this bill?')) { |
|
|
savedBills = savedBills.filter(bill => bill.id !== id); |
|
|
localStorage.setItem('savedBills', JSON.stringify(savedBills)); |
|
|
renderSavedBills(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
taxInput.addEventListener('change', updateCalculations); |
|
|
discountInput.addEventListener('change', updateCalculations); |
|
|
|
|
|
|
|
|
renderItemsTable(); |
|
|
renderSavedBills(); |
|
|
|
|
|
|
|
|
const customerInputs = ['customer-name', 'customer-phone', 'customer-address']; |
|
|
customerInputs.forEach(id => { |
|
|
document.getElementById(id).addEventListener('input', updateInvoicePreview); |
|
|
}); |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|