488 lines
12 KiB
HTML
488 lines
12 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>Invoice {{invoiceNumber}}</title>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap');
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
padding: 20px 40px;
|
|
color: #1a1a1a;
|
|
line-height: 1.6;
|
|
max-width: 850px;
|
|
margin: 0 auto;
|
|
background-color: #fff;
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 40px;
|
|
padding-bottom: 20px;
|
|
border-bottom: 3px solid #002366;
|
|
}
|
|
|
|
.company-info {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
|
|
.logo-container {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.logo-container img {
|
|
width: 80px;
|
|
height: 80px;
|
|
object-fit: cover;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.company-text {
|
|
flex: 1;
|
|
}
|
|
|
|
.company-name {
|
|
font-size: 26px;
|
|
font-weight: 700;
|
|
color: #002366;
|
|
margin-bottom: 8px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.company-details {
|
|
font-size: 12px;
|
|
color: #666;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.invoice-title {
|
|
text-align: right;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.invoice-title h1 {
|
|
font-size: 32px;
|
|
font-weight: 700;
|
|
color: #002366;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.invoice-number {
|
|
font-size: 14px;
|
|
color: #666;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.invoice-meta {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 30px;
|
|
gap: 20px;
|
|
}
|
|
|
|
.meta-section {
|
|
flex: 1;
|
|
}
|
|
|
|
.meta-section h3 {
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
color: #999;
|
|
margin-bottom: 10px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.meta-content {
|
|
font-size: 13px;
|
|
color: #333;
|
|
}
|
|
|
|
.meta-content div {
|
|
margin-bottom: 3px;
|
|
}
|
|
|
|
.customer-name {
|
|
font-weight: 600;
|
|
font-size: 15px;
|
|
color: #1a1a1a;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.dates-section {
|
|
text-align: right;
|
|
}
|
|
|
|
.date-item {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.date-label {
|
|
font-size: 11px;
|
|
color: #999;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.date-value {
|
|
font-size: 13px;
|
|
color: #333;
|
|
font-weight: 500;
|
|
text-transform: capitalize;
|
|
}
|
|
|
|
.items-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.items-table thead {
|
|
background-color: #e6f0ff;
|
|
}
|
|
|
|
.items-table th {
|
|
padding: 12px 10px;
|
|
text-align: left;
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
color: #666;
|
|
font-weight: 600;
|
|
border-bottom: 1px solid #002366;
|
|
}
|
|
|
|
.items-table th:last-child,
|
|
.items-table td:last-child {
|
|
text-align: right;
|
|
}
|
|
|
|
.items-table tbody tr {
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.items-table tbody tr:hover {
|
|
background-color: #fafafa;
|
|
}
|
|
|
|
.items-table td {
|
|
padding: 14px 10px;
|
|
font-size: 13px;
|
|
color: #333;
|
|
}
|
|
|
|
.item-description {
|
|
font-weight: 500;
|
|
color: #1a1a1a;
|
|
}
|
|
|
|
.totals-section {
|
|
margin-left: auto;
|
|
width: 320px;
|
|
padding: 18px;
|
|
background-color: #f9f9f9;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.total-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 8px 0;
|
|
}
|
|
|
|
.total-row.subtotal {
|
|
color: #666;
|
|
padding-bottom: 8px;
|
|
}
|
|
|
|
.total-row.grand-total {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: #002366;
|
|
border-top: 2px solid #002366;
|
|
padding-top: 12px;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.footer {
|
|
margin-top: 40px;
|
|
padding-top: 20px;
|
|
border-top: 1px solid #eee;
|
|
font-size: 12px;
|
|
color: #999;
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.notes-section {
|
|
margin-top: 30px;
|
|
padding: 18px;
|
|
background-color: #f9f9f9;
|
|
border-left: 4px solid #002366;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.notes-section h3 {
|
|
font-size: 13px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
color: #666;
|
|
margin-bottom: 8px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.notes-section p {
|
|
font-size: 12px;
|
|
color: #555;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
@media print {
|
|
body {
|
|
padding: 10px 20px;
|
|
}
|
|
|
|
.items-table tbody tr:hover {
|
|
background-color: transparent;
|
|
}
|
|
}
|
|
</style>
|
|
<script>
|
|
// Format numbers with Indian comma system (lakhs and crores)
|
|
function formatIndianNumber(num) {
|
|
if (!num) return num;
|
|
|
|
// Remove existing formatting
|
|
let numStr = num.toString().replace(/,/g, '');
|
|
|
|
// Check if it's a number
|
|
if (isNaN(numStr)) return num;
|
|
|
|
// Split into integer and decimal parts
|
|
let parts = numStr.split('.');
|
|
let intPart = parts[0];
|
|
let decPart = parts[1] ? '.' + parts[1] : '';
|
|
|
|
// Indian numbering system: last 3 digits, then groups of 2
|
|
let lastThree = intPart.substring(intPart.length - 3);
|
|
let otherNumbers = intPart.substring(0, intPart.length - 3);
|
|
|
|
if (otherNumbers !== '') {
|
|
lastThree = ',' + lastThree;
|
|
}
|
|
|
|
let formatted = otherNumbers.replace(/\B(?=(\d{2})+(?!\d))/g, ',') + lastThree + decPart;
|
|
return formatted;
|
|
}
|
|
|
|
// Format all numbers when page loads
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Format all table cells with numbers
|
|
document.querySelectorAll('.items-table td').forEach(function(cell) {
|
|
let text = cell.textContent.trim();
|
|
let formatted = formatIndianNumber(text);
|
|
if (formatted !== text) {
|
|
cell.textContent = formatted;
|
|
}
|
|
});
|
|
|
|
// Format totals section
|
|
document.querySelectorAll('.totals-section .total-row span').forEach(function(span) {
|
|
let text = span.textContent.trim();
|
|
// Don't format if it contains currency or labels
|
|
if (!text.includes('Subtotal') && !text.includes('Tax') && !text.includes('Total')) {
|
|
let formatted = formatIndianNumber(text.replace(/[^\d.]/g, ''));
|
|
if (formatted !== text && formatted) {
|
|
span.textContent = text.replace(/[\d,]+\.?\d*/g, formatted);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Hide notes section if empty or only contains <span></span>
|
|
let notesSection = document.querySelector('.notes-section');
|
|
if (notesSection) {
|
|
let notesContent = notesSection.querySelector('p');
|
|
if (notesContent) {
|
|
let content = notesContent.innerHTML.trim();
|
|
if (content === '' || content === '<span></span>') {
|
|
notesSection.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fix subtotal calculation when tax is present
|
|
setTimeout(function() {
|
|
let totalRows = document.querySelectorAll('.total-row');
|
|
let subtotalSpan = null;
|
|
let taxSpan = null;
|
|
let totalSpan = null;
|
|
|
|
totalRows.forEach(function(row) {
|
|
if (row.classList.contains('subtotal')) {
|
|
subtotalSpan = row.querySelectorAll('span')[1];
|
|
} else if (row.classList.contains('grand-total')) {
|
|
totalSpan = row.querySelectorAll('span')[1];
|
|
} else {
|
|
// This is the tax row
|
|
taxSpan = row.querySelectorAll('span')[1];
|
|
}
|
|
});
|
|
|
|
if (subtotalSpan && taxSpan && totalSpan) {
|
|
// Extract numeric values
|
|
let subtotalText = subtotalSpan.textContent.trim().replace(/,/g, '');
|
|
let taxText = taxSpan.textContent.trim().replace(/,/g, '');
|
|
let totalText = totalSpan.textContent.trim().split(' ')[0].replace(/,/g, '');
|
|
|
|
let subtotalNum = parseFloat(subtotalText);
|
|
let taxNum = parseFloat(taxText);
|
|
let totalNum = parseFloat(totalText);
|
|
|
|
console.log('Subtotal:', subtotalNum, 'Tax:', taxNum, 'Total:', totalNum);
|
|
|
|
// If subtotal equals total, calculate actual subtotal (total - tax)
|
|
if (Math.abs(subtotalNum - totalNum) < 1 && taxNum > 0) {
|
|
let actualSubtotal = totalNum - taxNum;
|
|
subtotalSpan.textContent = formatIndianNumber(actualSubtotal.toFixed(2));
|
|
}
|
|
}
|
|
}, 100);
|
|
});
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<div class="company-info">
|
|
{{#logoUrl}}
|
|
<div class="logo-container">
|
|
<img src="{{logoUrl}}" alt="Company Logo">
|
|
</div>
|
|
{{/logoUrl}}
|
|
<div class="company-text">
|
|
<div class="company-name">{{companyName}}</div>
|
|
<div class="company-details">
|
|
{{#companyAddress}}
|
|
<div>{{companyAddress}}</div>
|
|
{{/companyAddress}}
|
|
{{#companyEmail}}
|
|
<div>{{companyEmail}}</div>
|
|
{{/companyEmail}}
|
|
{{#companyPhone}}
|
|
<div>{{companyPhone}}</div>
|
|
{{/companyPhone}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="invoice-title">
|
|
<h1><strong>Invoice</strong></h1>
|
|
<div class="invoice-number">#{{invoiceNumber}}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="invoice-meta">
|
|
<div class="meta-section">
|
|
<h3>Bill To</h3>
|
|
<div class="meta-content">
|
|
<div class="customer-name">{{customerName}}</div>
|
|
{{#customerAddress}}
|
|
<div>{{customerAddress}}</div>
|
|
{{/customerAddress}}
|
|
{{#customerEmail}}
|
|
<div>{{customerEmail}}</div>
|
|
{{/customerEmail}}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="meta-section dates-section">
|
|
<div class="date-item">
|
|
<div class="date-label">Issue Date</div>
|
|
<div class="date-value">{{issueDate}}</div>
|
|
</div>
|
|
{{#dueDate}}
|
|
<div class="date-item">
|
|
<div class="date-label">Due Date</div>
|
|
<div class="date-value">{{dueDate}}</div>
|
|
</div>
|
|
{{/dueDate}}
|
|
{{#status}}
|
|
<div class="date-item">
|
|
<div class="date-label">Status</div>
|
|
<div class="date-value">{{status}}</div>
|
|
</div>
|
|
{{/status}}
|
|
</div>
|
|
</div>
|
|
|
|
<table class="items-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Description</th>
|
|
<th>Quantity</th>
|
|
<th>Unit Price</th>
|
|
<th>Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{#items}}
|
|
<tr>
|
|
<td class="item-description">{{description}}</td>
|
|
<td>{{quantity}}</td>
|
|
<td>{{unitPrice}}</td>
|
|
<td>{{lineTotal}}</td>
|
|
</tr>
|
|
{{/items}}
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="totals-section">
|
|
{{#hasTax}}
|
|
<div class="total-row subtotal">
|
|
<span>Subtotal</span>
|
|
<span>{{subtotal}}</span>
|
|
</div>
|
|
<div class="total-row">
|
|
<span>Tax {{#taxRate}}({{taxRate}}%){{/taxRate}}</span>
|
|
<span>{{taxAmount}}</span>
|
|
</div>
|
|
{{/hasTax}}
|
|
{{#hasDiscount}}
|
|
<div class="total-row">
|
|
<span>Discount {{#discountPercentage}}({{discountPercentage}}%){{/discountPercentage}}</span>
|
|
<span>-{{discountAmount}}</span>
|
|
</div>
|
|
{{/hasDiscount}}
|
|
<div class="total-row grand-total">
|
|
<span>Total</span>
|
|
<span>{{total}} {{currency}}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{{#notes}}
|
|
<div class="notes-section">
|
|
<h3>Notes</h3>
|
|
<p>{{notes}}</p>
|
|
</div>
|
|
{{/notes}}
|
|
|
|
<div class="footer">
|
|
<div>Thank you for your business!</div>
|
|
<div>{{companyName}} Generated on {{issueDate}}</div>
|
|
</div>
|
|
</body>
|
|
</html> |