Here is the code
Click or tap to copy
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Complete Invoice System</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></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/jspdf-autotable/3.5.23/jspdf.plugin.autotable.min.js"></script>
<script src="https://unpkg.com/dexie@latest/dist/dexie.min.js"></script>
<style>
* { box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background: #f5f5f5;
touch-action: manipulation;
}
.container { max-width: 1400px; margin: auto; }
/* Navigation Menu Styles */
.nav-menu {
background: linear-gradient(135deg, #007bff, #0056b3);
padding: 15px 20px;
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.nav-menu h2 {
color: white;
margin: 0 0 15px 0;
text-align: center;
}
.nav-buttons {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.nav-btn {
background: rgba(255,255,255,0.2);
color: white;
border: 2px solid rgba(255,255,255,0.3);
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
backdrop-filter: blur(10px);
}
.nav-btn:hover {
background: rgba(255,255,255,0.3);
transform: translateY(-2px);
}
.nav-btn.active {
background: white;
color: #007bff;
border-color: white;
}
.section { display: none; }
.section.active { display: block; }
.input-section {
background: white;
padding: 20px;
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.invoice-preview {
background: white;
padding: 0;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
h3 { margin-top: 0; color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px; }
.form-row { display: flex; gap: 15px; margin-bottom: 15px; flex-wrap: wrap; }
.form-group { flex: 1; min-width: 200px; position: relative; }
.form-group label { display: block; margin-bottom: 5px; font-weight: bold; color: #555; }
.form-group input, .form-group textarea, .form-group select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group textarea { resize: vertical; height: 80px; }
.auto-populated {
background-color: #e8f5e8 !important;
border-color: #28a745 !important;
transition: background-color 0.5s ease;
}
.dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ddd;
border-top: none;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.dropdown-item {
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #eee;
}
.dropdown-item:hover {
background: #f5f5f5;
}
.items-section { margin-top: 20px; }
.items-table {
width: 100%;
border-collapse: collapse;
background: #f8f9fa;
border-radius: 5px;
overflow: hidden;
}
.items-table th, .items-table td {
padding: 10px;
border: 1px solid #ddd;
text-align: left;
font-size: 14px;
}
.items-table th {
background: #e9ecef;
font-weight: bold;
color: #333;
}
.items-table input, .items-table select {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.items-table .code-input { min-width: 100px; }
.items-table .desc-input { min-width: 200px; }
.items-table .qty-input, .items-table .rate-input, .items-table .sgst-input, .items-table .cgst-input, .items-table .total-input { min-width: 80px; }
.items-table .disc-type-select { min-width: 60px; }
.items-table .disc-input { min-width: 80px; }
.items-table .unit-select { min-width: 80px; }
.items-table .total-input {
background: #f0f0f0;
text-align: right;
pointer-events: none;
}
.items-table .remove-btn {
background: #dc3545;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
width: 100%;
}
.items-table .remove-btn:hover { background: #c82333; }
.add-item-btn, .generate-btn, .browse-items-btn, .add-stock-btn, .add-customer-btn,
.backup-btn, .restore-btn, .save-btn, .export-csv-btn, .search-customer-btn {
background: #28a745;
color: white;
border: none;
padding: 12px 20px;
border-radius: 5px;
cursor: pointer;
margin-right: 10px;
margin-top: 10px;
font-weight: bold;
transition: all 0.3s;
}
.add-item-btn:hover, .add-stock-btn:hover, .add-customer-btn:hover,
.backup-btn:hover, .restore-btn:hover, .save-btn:hover, .export-csv-btn:hover, .search-customer-btn:hover {
background: #218838;
transform: translateY(-2px);
}
.browse-items-btn { background: #17a2b8; }
.browse-items-btn:hover { background: #138496; }
.generate-btn { background: #007bff; }
.generate-btn:hover { background: #0056b3; }
.save-btn { background: #ffc107; color: #333; }
.save-btn:hover { background: #e0a800; }
.export-csv-btn { background: #17a2b8; }
.export-csv-btn:hover { background: #138496; }
.stock-table, .customer-table, .customer-history-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.stock-table th, .stock-table td, .customer-table th, .customer-table td, .customer-history-table th, .customer-history-table td {
border: 1px solid #ddd;
padding: 12px;
text-align: left;
}
.stock-table th, .customer-table th, .customer-history-table th {
background: #f8f9fa;
font-weight: bold;
}
.stock-table tr:hover, .customer-table tr:hover, .customer-history-table tr:hover {
background: #f5f5f5;
}
.customer-history-table .items-details {
display: none;
background: #f8f9fa;
}
.customer-history-table .items-details.active {
display: table-row;
}
.edit-btn, .delete-btn, .toggle-items-btn {
padding: 6px 12px;
margin: 2px;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
}
.edit-btn { background: #ffc107; color: #333; }
.edit-btn:hover { background: #e0a800; }
.delete-btn { background: #dc3545; color: white; }
.delete-btn:hover { background: #c82333; }
.toggle-items-btn { background: #17a2b8; color: white; }
.toggle-items-btn:hover { background: #138496; }
.low-stock {
color: #dc3545;
font-weight: bold;
}
.quantity-error, .disc-error {
border: 2px solid #dc3545 !important;
background-color: #fff3f3 !important;
}
/* Invoice Styles */
.invoice-box {
max-width: 700px;
margin: auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background: white;
position: relative;
}
.company-header {
text-align: center;
margin-bottom: 20px;
}
.company-header h2 { margin: 0; color: #333; }
.company-header p { margin: 2px 0; font-size: 13px; color: #666; }
.header { display: flex; justify-content: space-between; margin-bottom: 20px; }
.bill-to { font-size: 14px; }
.invoice-details { text-align: right; font-size: 14px; }
.invoice-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.invoice-table, .invoice-table th, .invoice-table td { border: 1px solid #ccc; }
.invoice-table th, .invoice-table td { padding: 8px; text-align: left; }
.invoice-table th { background: #f8f9fa; }
.total { text-align: right; font-weight: bold; margin-top: 10px; font-size: 16px; }
.print-btn {
margin-top: 20px;
padding: 10px 20px;
background: #007bff;
color: #fff;
border: none;
cursor: pointer;
border-radius: 5px;
font-weight: bold;
}
.print-btn:hover { background: #0056b3; }
/* Logo Styles */
.invoice-logo {
position: absolute;
top: 20px;
left: 20px;
z-index: 10;
cursor: move;
user-select: none;
-webkit-user-select: none;
touch-action: none;
}
.invoice-logo img {
width: 100%;
height: 100%;
object-fit: contain;
}
.invoice-logo.active {
border: 2px dashed #007bff;
}
.resize-handle {
position: absolute;
width: 12px;
height: 12px;
background: #007bff;
border-radius: 50%;
display: none;
}
.invoice-logo.active .resize-handle {
display: block;
}
.resize-handle.top-left {
top: -6px;
left: -6px;
cursor: nwse-resize;
}
.resize-handle.top-right {
top: -6px;
right: -6px;
cursor: nesw-resize;
}
.resize-handle.bottom-left {
bottom: -6px;
left: -6px;
cursor: nesw-resize;
}
.resize-handle.bottom-right {
bottom: -6px;
right: -6px;
cursor: nwse-resize;
}
.logo-controls {
margin-top: 20px;
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.logo-controls input[type="number"] {
width: 80px;
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
}
.logo-controls button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.reset-logo-btn {
background: #6c757d;
color: white;
}
.reset-logo-btn:hover {
background: #5a6268;
}
.remove-logo-btn {
background: #dc3545;
color: white;
}
.remove-logo-btn:hover {
background: #c82333;
}
/* Floating Dialog Styles */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: none;
align-items: center;
justify-content: center;
}
.dialog {
background: white;
border-radius: 12px;
padding: 0;
width: 90%;
max-width: 600px;
min-width: 300px;
max-height: 80vh;
min-height: 200px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
position: relative;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px 10px 20px;
border-bottom: 2px solid #e9ecef;
background: #f8f9fa;
-webkit-user-select: none;
user-select: none;
touch-action: none;
}
.dialog-controls {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.size-inputs {
display: flex;
gap: 5px;
align-items: center;
margin-right: 10px;
}
.size-input {
width: 60px;
padding: 2px 5px;
border: 1px solid #ddd;
border-radius: 3px;
font-size: 0.8rem;
text-align: center;
}
.resize-btn, .apply-btn {
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}
.resize-btn {
background: #6c757d;
color: white;
font-weight: 600;
}
.resize-btn:hover { background: #5a6268; }
.apply-btn { background: #007bff; color: white; }
.apply-btn:hover { background: #0056b3; }
.dialog-header h3 { color: #495057; margin: 0; border: none; }
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #6c757d;
padding: 5px;
border-radius: 50%;
width: 35px;
height: 35px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover { background: #f8f9fa; color: #495057; }
.search-section { margin-bottom: 20px; padding: 0 20px; }
.search-input {
width: 100%;
padding: 12px;
border: 2px solid #dee2e6;
border-radius: 8px;
font-size: 1rem;
}
.search-input:focus {
outline: none;
border-color: #4facfe;
box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.1);
}
.items-container {
max-height: calc(100% - 150px);
overflow-y: auto;
border: 1px solid #dee2e6;
border-radius: 8px;
margin: 0 20px 20px 20px;
}
.item-table { width: 100%; border-collapse: collapse; }
.item-table th, .item-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #dee2e6;
}
.item-table th {
background: #f8f9fa;
position: sticky;
top: 0;
font-weight: 600;
color: #495057;
}
.item-table tbody tr {
cursor: pointer;
transition: background-color 0.2s ease;
}
.item-table tbody tr:hover { background-color: #e3f2fd; }
.status-message {
padding: 10px;
margin: 10px 20px;
border-radius: 6px;
font-size: 0.9rem;
display: none;
}
.status-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.status-error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
@media print {
body { background: white; }
.input-section, .nav-menu { display: none; }
.invoice-preview { box-shadow: none; }
.dialog-overlay { display: none !important; }
.invoice-logo.active { border: none !important; }
.resize-handle { display: none !important; }
}
@media (max-width: 768px) {
.form-row { flex-direction: column; }
.items-section, .stock-table, .customer-table, .customer-history-table {
overflow-x: auto;
display: block;
}
.items-table th, .items-table td { min-width: 60px; }
.items-table .desc-input { min-width: 120px; }
.items-table .remove-btn { min-width: 80px; }
.dialog { width: 90%; min-width: 250px; max-width: 95vw; }
.dialog-header { padding: 10px 15px; }
.size-input { width: 50px; font-size: 0.7rem; }
.resize-btn, .apply-btn { padding: 3px 6px; font-size: 0.7rem; }
.nav-buttons { flex-direction: column; align-items: center; }
.nav-btn { width: 200px; }
.logo-controls input[type="number"] { width: 60px; }
}
@media (max-width: 480px) {
.dialog { min-height: 150px; }
.items-container { max-height: calc(100% - 120px); }
.items-table th, .items-table td, .stock-table th, .stock-table td, .customer-table th, .customer-table td, .customer-history-table th, .customer-history-table td {
font-size: 12px;
padding: 6px;
}
.items-table input, .items-table select { font-size: 12px; padding: 4px; }
.item-table th, .item-table td { font-size: 12px; padding: 6px; }
.resize-handle { width: 10px; height: 10px; }
}
.hidden { display: none; }
.whatsapp-section {
background: #e8f5e8;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
border-left: 4px solid #25d366;
}
.whatsapp-section h3 {
color: #333;
border-bottom: 2px solid #25d366;
}
.btn-whatsapp {
background: #25d366;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
}
.btn-whatsapp:hover {
background: #20c360;
transform: translateY(-2px);
}
.whatsapp-preview {
max-height: 200px;
overflow-y: auto;
border: 1px solid #ddd;
margin-top: 15px;
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
white-space: pre-wrap;
font-family: monospace;
display: none;
}
.pdf-btn {
background: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
margin-right: 10px;
margin-top: 10px;
}
.pdf-btn:hover {
background: #0056b3;
}
.pdf-btn.share {
background: linear-gradient(135deg, #17a2b8, #20c997);
}
/* Dashboard Styles */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.dashboard-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
transition: transform 0.3s, box-shadow 0.3s;
}
.dashboard-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
}
.dashboard-card h4 {
margin: 0 0 10px;
color: #333;
font-size: 1.1rem;
}
.dashboard-card p {
margin: 0;
font-size: 1.5rem;
font-weight: bold;
color: white;
}
.top-products-list, .recent-orders-list {
max-height: 200px;
overflow-y: auto;
margin-top: 10px;
}
.top-products-list div, .recent-orders-list div {
padding: 10px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: background 0.2s;
}
.top-products-list div:hover, .recent-orders-list div:hover {
background: #f5f5f5;
}
.refresh-btn {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin-top: 10px;
}
.refresh-btn:hover {
background: #0056b3;
}
@media (max-width: 768px) {
.stats-grid, .dashboard-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<!-- Navigation Menu -->
<div class="nav-menu">
<h2>🇮🇳 Enhanced Indian Invoice System</h2>
<div class="nav-buttons">
<button class="nav-btn active" data-section="invoice">📋 Invoice Generator</button>
<button class="nav-btn" data-section="dashboard">📊 Sales Dashboard</button>
<button class="nav-btn" data-section="stock">📦 Stock Management</button>
<button class="nav-btn" data-section="customers">👥 Customer Database</button>
<button class="nav-btn" data-section="customer-history">📜 Customer History</button>
<button class="nav-btn" data-section="backup-restore">💾 Backup & Restore</button>
</div>
</div>
<!-- Sales Dashboard Section -->
<div class="section" id="dashboard">
<div class="input-section">
<h3>📊 Sales Dashboard</h3>
<div class="form-row">
<div class="form-group">
<label>Date Range</label>
<select id="dateRangeFilter" onchange="toggleCustomDateRange()">
<option value="7">Last 7 days</option>
<option value="30">Last 30 days</option>
<option value="90">Last 90 days</option>
<option value="365">Last 365 days</option>
<option value="custom">Custom Range</option>
</select>
</div>
<div class="form-group hidden" id="customDateRange">
<label>From</label>
<input type="date" id="fromDate">
<label>To</label>
<input type="date" id="toDate">
</div>
<div class="form-group">
<button class="refresh-btn" onclick="updateDashboard()">Refresh</button>
<button class="refresh-btn" onclick="clearInvoices()">Clear Invoices</button>
</div>
</div>
<div class="stats-grid">
<div class="dashboard-card" style="background: linear-gradient(135deg, #28a745, #218838);">
<h4>Total Revenue</h4>
<p id="totalRevenue">₹0.00</p>
</div>
<div class="dashboard-card" style="background: linear-gradient(135deg, #007bff, #0056b3);">
<h4>Total Orders</h4>
<p id="totalOrders">0</p>
</div>
<div class="dashboard-card" style="background: linear-gradient(135deg, #ff4d4f, #cf1322);">
<h4>Active Customers</h4>
<p id="totalCustomers">0</p>
</div>
<div class="dashboard-card" style="background: linear-gradient(135deg, #17a2b8, #117a8b);">
<h4>Products Sold</h4>
<p id="totalProducts">0</p>
</div>
<div class="dashboard-card" style="background: linear-gradient(135deg, #ffc107, #e0a800);">
<h4>Total Discounts</h4>
<p id="totalDiscount">₹0.00</p>
</div>
</div>
<div class="dashboard-grid">
<div class="dashboard-card">
<h4>Top Selling Products</h4>
<div class="top-products-list" id="topProductsList"></div>
</div>
<div class="dashboard-card">
<h4>Recent Orders</h4>
<div class="recent-orders-list" id="recentOrdersList"></div>
</div>
</div>
</div>
</div>
<!-- Invoice Generator Section -->
<div class="section active" id="invoice">
<div class="input-section">
<h3>📝 Invoice Generator</h3>
<!-- Company Info -->
<div class="form-row">
<div class="form-group">
<label>Company Name</label>
<input type="text" id="companyName" value="Your Company Name">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Company Address</label>
<textarea id="companyAddress">123 Business Street, City, State - ZIP</textarea>
</div>
<div class="form-group">
<label>Contact Info</label>
<textarea id="companyContact">Email: info@company.com
Phone: +91 9037393709</textarea>
</div>
</div>
<!-- Logo Upload -->
<h3>🖼️ Logo</h3>
<div class="form-row">
<div class="form-group">
<label>Upload Logo</label>
<input type="file" id="logoUpload" accept="image/*">
</div>
<div class="form-group logo-controls">
<label>Width (px)</label>
<input type="number" id="logoWidth" min="20" max="300" placeholder="Width">
<label>Height (px)</label>
<input type="number" id="logoHeight" min="20" max="300" placeholder="Height">
<label><input type="checkbox" id="lockAspectRatio" checked> Lock Aspect Ratio</label>
<button class="reset-logo-btn" onclick="resetLogoPosition()">Reset Position</button>
<button class="remove-logo-btn" onclick="removeLogo()">Remove Logo</button>
</div>
</div>
<p style="font-size: 12px; color: #666;">Tip: Tap logo to show handles, drag to move, or tap handles to resize. Double-tap logo to hide handles.</p>
<!-- Bill To -->
<h3>👤 Bill To</h3>
<div class="form-row">
<div class="form-group">
<label>Client Contact Number</label>
<input type="text" id="clientContact" placeholder="Enter contact number">
<div class="dropdown" id="contactDropdown"></div>
</div>
<div class="form-group">
<label>Client Name</label>
<input type="text" id="clientName" placeholder="Name will auto-populate">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Client Email</label>
<input type="email" id="clientEmail" placeholder="Email will auto-populate">
</div>
<div class="form-group">
<label>Client Address</label>
<textarea id="clientAddress" placeholder="Address will auto-populate"></textarea>
</div>
</div>
<!-- Invoice Details -->
<h3>📋 Invoice Details</h3>
<div class="form-row">
<div class="form-group">
<label>Invoice Number</label>
<input type="text" id="invoiceNumber" value="INV-001">
</div>
<div class="form-group">
<label>Invoice Date</label>
<input type="date" id="invoiceDate">
</div>
<div class="form-group">
<label>Due Date</label>
<input type="date" id="dueDate">
</div>
</div>
<!-- Items Section -->
<h3>📦 Items</h3>
<div class="items-section">
<table class="items-table" id="itemsList">
<thead>
<tr>
<th>HSN/SAC Code</th>
<th>Description</th>
<th>Unit</th>
<th>Qty</th>
<th>Rate (₹)</th>
<th>Disc Type</th>
<th>Discount</th>
<th>SGST %</th>
<th>CGST %</th>
<th>Total (₹)</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr class="item-row">
<td><input type="text" class="item-code code-input" placeholder="Enter HSN/SAC code..."><div class="dropdown item-dropdown"></div></td>
<td><input type="text" class="item-desc desc-input" placeholder="Service or product description"></td>
<td><select class="item-unit unit-select"><option value="PCS">PCS</option><option value="KG">KG</option><option value="LTR">LTR</option><option value="MTR">MTR</option><option value="BOX">BOX</option><option value="SET">SET</option></select></td>
<td><input type="number" class="item-qty qty-input" value="1" min="1"></td>
<td><input type="number" class="item-rate rate-input" value="0" step="0.01" min="0"></td>
<td>
<select class="disc-type-select">
<option value="percent">%</option>
<option value="amount">₹</option>
</select>
</td>
<td><input type="number" class="item-disc disc-input" value="0" step="0.01" min="0"></td>
<td><input type="number" class="item-sgst sgst-input" value="0" step="0.01" min="0"></td>
<td><input type="number" class="item-cgst cgst-input" value="0" step="0.01" min="0"></td>
<td><input type="text" class="item-total total-input" value="0.00" readonly></td>
<td><button class="remove-btn" onclick="removeItem(this)">Remove</button></td>
</tr>
</tbody>
</table>
<button class="add-item-btn" onclick="addItem()">+ Add Item</button>
<button class="browse-items-btn" onclick="openItemDialog()">📋 Browse Items</button>
<button class="generate-btn" onclick="updateItemTotals()">Generate Invoice</button>
<button class="save-btn" onclick="saveInvoice()">Save Invoice</button>
</div>
<!-- WhatsApp Section -->
<div class="whatsapp-section">
<h3>Send Invoice via WhatsApp</h3>
<div class="form-row">
<div class="form-group">
<label>WhatsApp Number</label>
<input type="text" id="whatsappNumber" placeholder="Enter phone number (e.g., +919037393709)">
</div>
<div class="form-group">
<label>Max Description Length</label>
<input type="number" id="whatsappDescLength" value="20" min="10" max="50">
</div>
<div class="form-group">
<label><input type="checkbox" id="gstEnabled" checked> Include GST in Message</label>
</div>
</div>
<button class="btn-whatsapp" onclick="generateWhatsAppMessage()">Preview WhatsApp Message</button>
<div class="whatsapp-preview" id="whatsappPreview" style="display: none;"></div>
<button class="btn-whatsapp" style="margin-top: 10px;" onclick="sendWhatsApp()">Send via WhatsApp</button>
</div>
<div class="pdf-buttons">
<button class="pdf-btn download" onclick="generatePDF()">Download PDF</button>
<button class="pdf-btn share" onclick="sharePDF()">Share PDF via WhatsApp</button>
<button class="pdf-btn print" onclick="printInvoice()">Print Invoice</button>
</div>
</div>
<!-- Invoice Preview -->
<div class="invoice-preview" id="invoicePreview">
<div class="invoice-box">
<div class="company-header">
<h2 id="previewCompanyName">Your Company Name</h2>
<p id="previewCompanyAddress">123 Business Street, City, State - ZIP</p>
<p id="previewCompanyContact">Email: info@company.com | Phone: +91 9037393709</p>
</div>
<div class="header">
<div class="bill-to">
<b>Bill To</b><br>
<span id="previewClientName">Customer Name</span><br>
<span id="previewClientAddress">Customer Address</span><br>
Contact: <span id="previewClientContact">Customer Contact</span><br>
Email: <span id="previewClientEmail">Customer Email</span>
</div>
<div class="invoice-details">
<b style="color:red;">INVOICE</b><br>
Invoice #: <span id="previewInvoiceNumber">INV-001</span><br>
Invoice Date: <span id="previewInvoiceDate">27 August 2025</span><br>
Due Date: <span id="previewDueDate">26 September 2025</span>
</div>
</div>
<table class="invoice-table">
<thead>
<tr>
<th>Code</th>
<th>Description</th>
<th>Unit</th>
<th>Qty</th>
<th>Rate</th>
<th>Discount</th>
<th>SGST %</th>
<th>CGST %</th>
<th>Total</th>
</tr>
</thead>
<tbody id="previewItems">
<tr>
<td></td>
<td>Sample Item</td>
<td>PCS</td>
<td>1</td>
<td>₹0.00</td>
<td>0%</td>
<td>0</td>
<td>0</td>
<td>₹0.00</td>
</tr>
</tbody>
</table>
<p class="total">Grand Total: <span id="previewTotal">₹0.00</span></p>
<button class="print-btn" onclick="window.print()">Print / Save as PDF</button>
</div>
</div>
</div>
<!-- Stock Management Section -->
<div class="section" id="stock">
<div class="input-section">
<h3>📦 Stock Management</h3>
<div class="form-row">
<div class="form-group">
<label>Item Code</label>
<input type="text" id="stockCode" placeholder="e.g., ITM001">
</div>
<div class="form-group">
<label>HSN/SAC Code</label>
<input type="text" id="stockHSN" placeholder="e.g., 8471">
</div>
<div class="form-group">
<label>Description</label>
<input type="text" id="stockDescription" placeholder="e.g., Ergonomic wireless mouse">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Rate (₹)</label>
<input type="number" id="stockPrice" placeholder="e.g., 25.99" step="0.01" min="0">
</div>
<div class="form-group">
<label>Unit of Measurement</label>
<select id="stockUnit">
<option value="PCS">PCS - Pieces</option>
<option value="KG">KG - Kilogram</option>
<option value="LTR">LTR - Liter</option>
<option value="MTR">MTR - Meter</option>
<option value="BOX">BOX - Box</option>
<option value="SET">SET - Set</option>
</select>
</div>
<div class="form-group">
<label>Quantity</label>
<input type="number" id="stockQuantity" min="0" placeholder="Enter quantity">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>SGST Rate (%)</label>
<input type="number" id="stockSGST" value="9" step="0.01" min="0">
</div>
<div class="form-group">
<label>CGST Rate (%)</label>
<input type="number" id="stockCGST" value="9" step="0.01" min="0">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Discount Type</label>
<select id="stockDiscountType">
<option value="percent">%</option>
<option value="amount">₹</option>
</select>
</div>
<div class="form-group">
<label>Discount</label>
<input type="number" id="stockDiscount" value="0" step="0.01" min="0">
</div>
</div>
<button class="add-stock-btn" onclick="addStockItem()">Add to Inventory</button>
<table class="stock-table" id="stockTable">
<thead>
<tr>
<th>Item Code</th>
<th>HSN/SAC</th>
<th>Description</th>
<th>Rate (₹)</th>
<th>Unit</th>
<th>SGST %</th>
<th>CGST %</th>
<th>Discount</th>
<th>Quantity</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="stockTableBody">
</tbody>
</table>
</div>
</div>
<!-- Customer Database Section -->
<div class="section" id="customers">
<div class="input-section">
<h3>👥 Customer Database</h3>
<div class="form-row">
<div class="form-group">
<label>Contact Number</label>
<input type="text" id="newCustomerContact" placeholder="Enter contact number">
</div>
<div class="form-group">
<label>Customer Name</label>
<input type="text" id="newCustomerName" placeholder="Enter customer name">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" id="newCustomerEmail" placeholder="Enter email">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Address</label>
<textarea id="newCustomerAddress" placeholder="Enter address"></textarea>
</div>
</div>
<button class="add-customer-btn" onclick="addCustomer()">+ Add Customer</button>
<table class="customer-table" id="customerTable">
<thead>
<tr>
<th>Contact</th>
<th>Name</th>
<th>Email</th>
<th>Address</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="customerTableBody">
</tbody>
</table>
</div>
</div>
<!-- Customer History Section -->
<div class="section" id="customer-history">
<div class="input-section">
<h3>📜 Customer History</h3>
<div class="form-row">
<div class="form-group">
<label>Customer Contact Number</label>
<input type="text" id="historyContact" placeholder="Enter contact number (e.g., +919037393709)">
<div class="dropdown" id="historyContactDropdown"></div>
</div>
<div class="form-group">
<button class="search-customer-btn" onclick="loadCustomerHistory()">Search History</button>
</div>
</div>
<table class="customer-history-table" id="customerHistoryTable">
<thead>
<tr>
<th>Invoice Number</th>
<th>Invoice Date</th>
<th>Customer Name</th>
<th>Total (₹)</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="customerHistoryTableBody">
</tbody>
</table>
<div class="status-message" id="historyStatusMessage"></div>
</div>
</div>
<!-- Backup & Restore Section -->
<div class="section" id="backup-restore">
<div class="input-section">
<h3>💾 Backup & Restore</h3>
<div class="form-row">
<div class="form-group">
<label>Backup All Data</label>
<button class="backup-btn" onclick="backupData()">Backup Data to JSON</button>
</div>
<div class="form-group">
<label>Restore All Data</label>
<input type="file" id="restoreFileInput" accept=".json">
<button class="restore-btn" onclick="restoreData()">Restore Data</button>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Backup Stock Data Only</label>
<button class="backup-btn" onclick="backupStockData()">Backup Stock to JSON</button>
</div>
<div class="form-group">
<label>Restore Stock Data Only</label>
<input type="file" id="restoreStockFileInput" accept=".json">
<button class="restore-btn" onclick="restoreStockData()">Restore Stock Data</button>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Backup Customer Data Only</label>
<button class="backup-btn" onclick="backupCustomerData()">Backup Customers to JSON</button>
</div>
<div class="form-group">
<label>Restore Customer Data Only</label>
<input type="file" id="restoreCustomerFileInput" accept=".json">
<button class="restore-btn" onclick="restoreCustomerData()">Restore Customer Data</button>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Export Invoices</label>
<button class="export-csv-btn" onclick="exportInvoicesToCSV()">Export Invoices to CSV</button>
</div>
</div>
<div class="status-message" id="backupStatusMessage"></div>
</div>
</div>
</div>
<!-- Item Selection Dialog -->
<div class="dialog-overlay" id="dialogOverlay" onclick="closeDialog(event, 'dialogOverlay')">
<div class="dialog" id="itemDialog" onclick="event.stopPropagation()">
<div class="dialog-header">
<h3>Select Items</h3>
<div class="dialog-controls">
<div class="size-inputs">
<span style="font-size: 0.8rem;">W:</span>
<input type="number" class="size-input" id="itemWidthInput" value="600" min="300" max="1200">
<span style="font-size: 0.8rem;">H:</span>
<input type="number" class="size-input" id="itemHeightInput" value="450" min="200" max="800">
<button class="apply-btn" onclick="applyCustomSize('itemDialog', 'itemWidthInput', 'itemHeightInput')">Apply</button>
</div>
<div class="resize-controls">
<button class="resize-btn" onclick="quickResize(400, 250, 'itemDialog', 'itemWidthInput', 'itemHeightInput')" title="Compact">XS</button>
<button class="resize-btn" onclick="quickResize(500, 350, 'itemDialog', 'itemWidthInput', 'itemHeightInput')" title="Small">S</button>
<button class="resize-btn" onclick="quickResize(700, 500, 'itemDialog', 'itemWidthInput', 'itemHeightInput')" title="Large">L</button>
</div>
<button class="close-btn" onclick="closeDialog(null, 'dialogOverlay')">×</button>
</div>
</div>
<div class="search-section">
<input type="text" class="search-input" id="searchInput" placeholder="Search items by code, name, or description..." oninput="searchItems()">
</div>
<div class="status-message" id="statusMessage"></div>
<div class="items-container">
<table class="item-table">
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Description</th>
<th>Price</th>
<th>SGST %</th>
<th>CGST %</th>
<th>Discount</th>
</tr>
</thead>
<tbody id="itemsList2">
</tbody>
</table>
</div>
</div>
</div>
<!-- Edit Customer Dialog -->
<div class="dialog-overlay" id="editCustomerDialogOverlay" onclick="closeDialog(event, 'editCustomerDialogOverlay')">
<div class="dialog" id="editCustomerDialog" onclick="event.stopPropagation()">
<div class="dialog-header">
<h3>Edit Customer</h3>
<button class="close-btn" onclick="closeDialog(null, 'editCustomerDialogOverlay')">×</button>
</div>
<div class="form-row" style="padding: 20px;">
<div class="form-group">
<label>Contact Number</label>
<input type="text" id="editCustomerContact" placeholder="Enter contact number">
</div>
<div class="form-group">
<label>Customer Name</label>
<input type="text" id="editCustomerName" placeholder="Enter customer name">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" id="editCustomerEmail" placeholder="Enter email">
</div>
<div class="form-group">
<label>Address</label>
<textarea id="editCustomerAddress" placeholder="Enter address"></textarea>
</div>
</div>
<div style="padding: 0 20px 20px;">
<button class="add-customer-btn" onclick="saveEditedCustomer()">Save Changes</button>
</div>
</div>
</div>
<script>
// Initialize IndexedDB with Dexie.js
const db = new Dexie('InvoiceDB');
db.version(1).stores({
invoices: 'invoiceNumber,clientContact,timestamp',
inventory: 'code,hsn,description',
customers: 'contact,name'
});
// Initialize data arrays
let inventory = [];
let customerDatabase = [];
let filteredInventory = [];
let invoices = [];
let editingCustomerContact = null;
// Default inventory data
const defaultInventory = [
{ code: 'ITM001', hsn: '8471', name: 'Wireless Mouse', description: 'Ergonomic wireless mouse with USB receiver', price: 25.99, unit: 'PCS', quantity: 50, sgst: 9, cgst: 9, discountType: 'percent', discount: 5 },
{ code: 'ITM002', hsn: '8471', name: 'Keyboard', description: 'Mechanical gaming keyboard with RGB lighting', price: 89.99, unit: 'PCS', quantity: 30, sgst: 9, cgst: 9, discountType: 'amount', discount: 10 },
{ code: 'ITM003', hsn: '9403', name: 'Monitor Stand', description: 'Adjustable aluminum monitor stand', price: 45.00, unit: 'PCS', quantity: 20, sgst: 9, cgst: 9, discountType: 'percent', discount: 0 },
{ code: 'ITM004', hsn: '8544', name: 'USB Cable', description: '6ft USB-C to USB-A cable', price: 12.50, unit: 'PCS', quantity: 100, sgst: 9, cgst: 9, discountType: 'amount', discount: 2.50 },
{ code: 'ITM005', hsn: '8525', name: 'Webcam', description: '1080p HD webcam with built-in microphone', price: 67.99, unit: 'PCS', quantity: 15, sgst: 9, cgst: 9, discountType: 'percent', discount: 10 }
];
// Default customer data
const defaultCustomers = [
{ contact: "+919037393709", name: "John Doe", email: "john@example.com", address: "123 Main St, City" },
{ contact: "+919876543210", name: "Jane Smith", email: "jane@example.com", address: "456 Park Ave, City" }
];
// Migrate data from localStorage to IndexedDB
async function migrateLocalStorageToIndexedDB() {
try {
const invoiceCount = await db.invoices.count();
if (invoiceCount > 0) return; // Skip migration if IndexedDB already has data
const localInventory = JSON.parse(localStorage.getItem('inventory'));
const localCustomers = JSON.parse(localStorage.getItem('customerDatabase'));
const localInvoices = JSON.parse(localStorage.getItem('invoices'));
if (localInventory && localInventory.length > 0) {
await db.inventory.bulkPut(localInventory);
} else {
await db.inventory.bulkPut(defaultInventory);
}
if (localCustomers && localCustomers.length > 0) {
await db.customers.bulkPut(localCustomers);
} else {
await db.customers.bulkPut(defaultCustomers);
}
if (localInvoices && localInvoices.length > 0) {
await db.invoices.bulkPut(localInvoices);
}
// Clear localStorage after migration
localStorage.removeItem('inventory');
localStorage.removeItem('customerDatabase');
localStorage.removeItem('invoices');
} catch (error) {
showStatus('Error during data migration: ' + error.message, 'error');
}
}
// Load data from IndexedDB
async function loadDataFromIndexedDB() {
try {
inventory = await db.inventory.toArray();
customerDatabase = await db.customers.toArray();
invoices = await db.invoices.toArray();
filteredInventory = [...inventory];
} catch (error) {
showStatus('Error loading data from IndexedDB: ' + error.message, 'error');
}
}
// Input sanitization function
function sanitizeInput(input) {
return input.replace(/[<>"'&]/g, '');
}
// Debounce function for autocomplete
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Generate unique invoice number
async function generateInvoiceNumber() {
try {
const invoices = await db.invoices.toArray();
const existingNumbers = invoices.map(inv => parseInt(inv.invoiceNumber.replace('INV-', '')));
const nextNumber = existingNumbers.length ? Math.max(...existingNumbers) + 1 : 1;
return `INV-${String(nextNumber).padStart(3, '0')}`;
} catch (error) {
showStatus('Error generating invoice number: ' + error.message, 'error');
return 'INV-001';
}
}
// Navigation functionality
function setupNavigation() {
const navButtons = document.querySelectorAll('.nav-btn');
navButtons.forEach(button => {
button.addEventListener('click', function() {
navButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
document.querySelectorAll('.section').forEach(section => section.classList.remove('active'));
document.getElementById(this.getAttribute('data-section')).classList.add('active');
});
});
}
// Show status message
function showStatus(message, type, elementId = 'statusMessage') {
const statusMessage = document.getElementById(elementId);
statusMessage.textContent = message;
statusMessage.className = `status-message status-${type}`;
statusMessage.style.display = 'block';
setTimeout(() => {
statusMessage.style.display = 'none';
}, 3000);
}
// Add new item row
function addItem() {
const itemsList = document.getElementById('itemsList').querySelector('tbody');
const newRow = document.createElement('tr');
newRow.className = 'item-row';
newRow.innerHTML = `
<td><input type="text" class="item-code code-input" placeholder="Enter HSN/SAC code..."><div class="dropdown item-dropdown"></div></td>
<td><input type="text" class="item-desc desc-input" placeholder="Service or product description"></td>
<td><select class="item-unit unit-select"><option value="PCS">PCS</option><option value="KG">KG</option><option value="LTR">LTR</option><option value="MTR">MTR</option><option value="BOX">BOX</option><option value="SET">SET</option></select></td>
<td><input type="number" class="item-qty qty-input" value="1" min="1"></td>
<td><input type="number" class="item-rate rate-input" value="0" step="0.01" min="0"></td>
<td>
<select class="disc-type-select">
<option value="percent">%</option>
<option value="amount">₹</option>
</select>
</td>
<td><input type="number" class="item-disc disc-input" value="0" step="0.01" min="0"></td>
<td><input type="number" class="item-sgst sgst-input" value="0" step="0.01" min="0"></td>
<td><input type="number" class="item-cgst cgst-input" value="0" step="0.01" min="0"></td>
<td><input type="text" class="item-total total-input" value="0.00" readonly></td>
<td><button class="remove-btn" onclick="removeItem(this)">Remove</button></td>
`;
itemsList.appendChild(newRow);
attachInputListeners(newRow);
setupItemAutocomplete(newRow);
updateItemTotals();
return newRow;
}
// Remove item row
function removeItem(button) {
const itemsList = document.getElementById('itemsList').querySelector('tbody');
if (itemsList.children.length > 1) {
button.closest('tr').remove();
updateItemTotals();
} else {
showStatus('At least one item is required.', 'error');
}
}
// Attach input listeners for real-time calculations
function attachInputListeners(row) {
row.querySelectorAll('.item-qty, .item-rate, .item-disc, .item-sgst, .item-cgst, .disc-type-select').forEach(input => {
input.addEventListener('input', () => {
const code = row.querySelector('.item-code').value;
const qty = parseInt(row.querySelector('.item-qty').value) || 0;
const disc = parseFloat(row.querySelector('.item-disc').value) || 0;
const discType = row.querySelector('.disc-type-select').value;
const rate = parseFloat(row.querySelector('.item-rate').value) || 0;
// Validate quantity
if (input.classList.contains('item-qty') && code) {
const item = inventory.find(i => i.code === code);
if (item && qty > item.quantity) {
input.classList.add('quantity-error');
showStatus(`Insufficient stock for ${item.description}. Available: ${item.quantity} units.`, 'error');
input.value = item.quantity;
} else {
input.classList.remove('quantity-error');
}
}
// Validate discount
if (input.classList.contains('item-disc') || input.classList.contains('disc-type-select')) {
if (discType === 'percent' && disc > 100) {
input.classList.add('disc-error');
showStatus('Discount percentage cannot exceed 100%.', 'error');
input.value = 100;
} else if (discType === 'amount' && disc > qty * rate) {
input.classList.add('disc-error');
showStatus('Discount amount cannot exceed subtotal.', 'error');
input.value = qty * rate;
} else {
input.classList.remove('disc-error');
}
}
updateItemTotal(row);
updateItemTotals();
});
});
}
// Update individual item total
function updateItemTotal(row) {
const qty = parseFloat(row.querySelector('.item-qty').value) || 0;
const rate = parseFloat(row.querySelector('.item-rate').value) || 0;
const discType = row.querySelector('.disc-type-select').value;
const disc = parseFloat(row.querySelector('.item-disc').value) || 0;
const sgst = parseFloat(row.querySelector('.item-sgst').value) || 0;
const cgst = parseFloat(row.querySelector('.item-cgst').value) || 0;
const subtotal = qty * rate;
let discAmount = discType === 'percent' ? subtotal * (disc / 100) : disc;
const afterDisc = subtotal - discAmount;
const sgstAmount = afterDisc * (sgst / 100);
const cgstAmount = afterDisc * (cgst / 100);
const lineTotal = afterDisc + sgstAmount + cgstAmount;
row.querySelector('.item-total').value = lineTotal.toFixed(2);
}
// Update all item totals and invoice preview
function updateItemTotals() {
const rows = document.querySelectorAll('.item-row');
let subtotal = 0, totalDiscount = 0, totalSGST = 0, totalCGST = 0, grandTotal = 0;
let canGenerate = true;
rows.forEach(row => {
const code = row.querySelector('.item-code').value;
const qty = parseInt(row.querySelector('.item-qty').value) || 0;
const disc = parseFloat(row.querySelector('.item-disc').value) || 0;
const discType = row.querySelector('.disc-type-select').value;
const rate = parseFloat(row.querySelector('.item-rate').value) || 0;
const item = inventory.find(i => i.code === code);
if (item && qty > item.quantity) {
canGenerate = false;
row.querySelector('.item-qty').classList.add('quantity-error');
showStatus(`Insufficient stock for ${item.description}. Available: ${item.quantity} units.`, 'error');
} else {
row.querySelector('.item-qty').classList.remove('quantity-error');
}
if (discType === 'percent' && disc > 100) {
canGenerate = false;
row.querySelector('.item-disc').classList.add('disc-error');
showStatus('Discount percentage cannot exceed 100%.', 'error');
} else if (discType === 'amount' && disc > qty * rate) {
canGenerate = false;
row.querySelector('.item-disc').classList.add('disc-error');
showStatus('Discount amount cannot exceed subtotal.', 'error');
} else {
row.querySelector('.item-disc').classList.remove('disc-error');
}
});
if (!canGenerate) return;
rows.forEach(row => {
const qty = parseFloat(row.querySelector('.item-qty').value) || 0;
const rate = parseFloat(row.querySelector('.item-rate').value) || 0;
const discType = row.querySelector('.disc-type-select').value;
const disc = parseFloat(row.querySelector('.item-disc').value) || 0;
const sgst = parseFloat(row.querySelector('.item-sgst').value) || 0;
const cgst = parseFloat(row.querySelector('.item-cgst').value) || 0;
const subtotalAmount = qty * rate;
let discAmount = discType === 'percent' ? subtotalAmount * (disc / 100) : disc;
const afterDisc = subtotalAmount - discAmount;
const sgstAmount = afterDisc * (sgst / 100);
const cgstAmount = afterDisc * (cgst / 100);
const lineTotal = afterDisc + sgstAmount + cgstAmount;
row.querySelector('.item-total').value = lineTotal.toFixed(2);
subtotal += subtotalAmount;
totalDiscount += discAmount;
totalSGST += sgstAmount;
totalCGST += cgstAmount;
grandTotal += lineTotal;
});
const totalsRow = `
<tr>
<td colspan="4"></td>
<td colspan="3">Subtotal</td>
<td>₹${subtotal.toFixed(2)}</td>
</tr>
<tr>
<td colspan="4"></td>
<td colspan="3">Total Discount</td>
<td>₹${totalDiscount.toFixed(2)}</td>
</tr>
<tr>
<td colspan="4"></td>
<td colspan="3">Total SGST</td>
<td>₹${totalSGST.toFixed(2)}</td>
</tr>
<tr>
<td colspan="4"></td>
<td colspan="3">Total CGST</td>
<td>₹${totalCGST.toFixed(2)}</td>
</tr>
<tr>
<td colspan="4"></td>
<td colspan="3"><strong>Grand Total</strong></td>
<td><strong>₹${grandTotal.toFixed(2)}</strong></td>
</tr>
`;
generateInvoice(totalsRow);
}
// Save invoice and update inventory
async function saveInvoice() {
try {
const invoice = {
invoiceNumber: document.getElementById('invoiceNumber').value || await generateInvoiceNumber(),
invoiceDate: document.getElementById('invoiceDate').value,
dueDate: document.getElementById('dueDate').value,
clientContact: sanitizeInput(document.getElementById('clientContact').value),
clientName: sanitizeInput(document.getElementById('clientName').value),
clientEmail: sanitizeInput(document.getElementById('clientEmail').value),
clientAddress: sanitizeInput(document.getElementById('clientAddress').value),
items: [],
subtotal: 0,
totalDiscount: 0,
totalSGST: 0,
totalCGST: 0,
grandTotal: 0,
timestamp: new Date().toISOString()
};
const rows = document.querySelectorAll('.item-row');
let canSave = true;
// Validate all rows
rows.forEach(row => {
const code = row.querySelector('.item-code').value;
const qty = parseInt(row.querySelector('.item-qty').value) || 0;
const disc = parseFloat(row.querySelector('.item-disc').value) || 0;
const discType = row.querySelector('.disc-type-select').value;
const rate = parseFloat(row.querySelector('.item-rate').value) || 0;
const item = inventory.find(i => i.code === code);
if (item && qty > item.quantity) {
canSave = false;
row.querySelector('.item-qty').classList.add('quantity-error');
showStatus(`Insufficient stock for ${item.description}. Available: ${item.quantity} units.`, 'error');
}
if (discType === 'percent' && disc > 100) {
canSave = false;
row.querySelector('.item-disc').classList.add('disc-error');
showStatus('Discount percentage cannot exceed 100%.', 'error');
} else if (discType === 'amount' && disc > qty * rate) {
canSave = false;
row.querySelector('.item-disc').classList.add('disc-error');
showStatus('Discount amount cannot exceed subtotal.', 'error');
}
});
if (!canSave) return;
// Build invoice items and update totals
rows.forEach(row => {
const code = row.querySelector('.item-code').value;
const qty = parseFloat(row.querySelector('.item-qty').value) || 0;
const unit = row.querySelector('.item-unit').value || 'PCS';
const rate = parseFloat(row.querySelector('.item-rate').value) || 0;
const discType = row.querySelector('.disc-type-select').value;
const disc = parseFloat(row.querySelector('.item-disc').value) || 0;
const sgst = parseFloat(row.querySelector('.item-sgst').value) || 0;
const cgst = parseFloat(row.querySelector('.item-cgst').value) || 0;
const subtotalAmount = qty * rate;
let discAmount = discType === 'percent' ? subtotalAmount * (disc / 100) : disc;
const afterDisc = subtotalAmount - discAmount;
const sgstAmount = afterDisc * (sgst / 100);
const cgstAmount = afterDisc * (cgst / 100);
const lineTotal = afterDisc + sgstAmount + cgstAmount;
invoice.items.push({
code: sanitizeInput(code),
description: sanitizeInput(row.querySelector('.item-desc').value),
quantity: qty,
unit: unit,
rate: rate,
discountType: discType,
discount: disc,
sgst: sgst,
cgst: cgst,
total: lineTotal
});
invoice.subtotal += subtotalAmount;
invoice.totalDiscount += discAmount;
invoice.totalSGST += sgstAmount;
invoice.totalCGST += cgstAmount;
invoice.grandTotal += lineTotal;
const item = inventory.find(i => i.code === code);
if (item) item.quantity -= qty;
});
if (!invoice.clientContact || !invoice.clientName || invoice.items.length === 0) {
showStatus('Please fill in client details and add at least one item.', 'error');
return;
}
// Save invoice and update inventory
await db.invoices.put(invoice);
await db.inventory.bulkPut(inventory);
invoices = await db.invoices.toArray();
loadStockTable();
showStatus('Invoice saved successfully!', 'success');
updateDashboard();
// Reset form
document.getElementById('invoiceNumber').value = await generateInvoiceNumber();
document.getElementById('clientContact').value = '';
document.getElementById('clientName').value = '';
document.getElementById('clientEmail').value = '';
document.getElementById('clientAddress').value = '';
document.getElementById('whatsappNumber').value = '';
const itemsList = document.getElementById('itemsList').querySelector('tbody');
itemsList.innerHTML = '';
addItem();
} catch (error) {
showStatus('Error saving invoice: ' + error.message, 'error');
}
}
// Generate invoice preview
function generateInvoice(totalsRow) {
const companyName = document.getElementById('companyName').value;
const companyAddress = document.getElementById('companyAddress').value;
const companyContact = document.getElementById('companyContact').value;
const clientName = document.getElementById('clientName').value;
const clientAddress = document.getElementById('clientAddress').value;
const clientContact = document.getElementById('clientContact').value;
const clientEmail = document.getElementById('clientEmail').value;
const invoiceNumber = document.getElementById('invoiceNumber').value;
const invoiceDate = document.getElementById('invoiceDate').value;
const dueDate = document.getElementById('dueDate').value;
document.getElementById('previewCompanyName').textContent = companyName || 'Your Company Name';
document.getElementById('previewCompanyAddress').textContent = companyAddress || '123 Business Street, City, State - ZIP';
document.getElementById('previewCompanyContact').textContent = companyContact || 'Email: info@company.com | Phone: +91 9037393709';
document.getElementById('previewClientName').textContent = clientName || 'Customer Name';
document.getElementById('previewClientAddress').textContent = clientAddress || 'Customer Address';
document.getElementById('previewClientContact').textContent = clientContact || 'Customer Contact';
document.getElementById('previewClientEmail').textContent = clientEmail || 'Customer Email';
document.getElementById('previewInvoiceNumber').textContent = invoiceNumber || 'INV-001';
document.getElementById('previewInvoiceDate').textContent = invoiceDate || new Date().toLocaleDateString();
document.getElementById('previewDueDate').textContent = dueDate || new Date().toLocaleDateString();
let itemsHtml = '';
document.querySelectorAll('.item-row').forEach(row => {
const code = row.querySelector('.item-code').value || '';
const desc = row.querySelector('.item-desc').value || 'Sample Item';
const unit = row.querySelector('.item-unit').value || 'PCS';
const qty = row.querySelector('.item-qty').value || 1;
const rate = parseFloat(row.querySelector('.item-rate').value) || 0;
const discType = row.querySelector('.disc-type-select').value;
const disc = parseFloat(row.querySelector('.item-disc').value) || 0;
const sgst = parseFloat(row.querySelector('.item-sgst').value) || 0;
const cgst = parseFloat(row.querySelector('.item-cgst').value) || 0;
const total = parseFloat(row.querySelector('.item-total').value) || 0;
const item = inventory.find(i => i.code === code);
const hsnCode = item ? item.hsn : code;
const discDisplay = discType === 'percent' ? `${disc}%` : `₹${disc.toFixed(2)}`;
itemsHtml += `
<tr>
<td>${sanitizeInput(hsnCode)}</td>
<td>${sanitizeInput(desc)}</td>
<td>${unit}</td>
<td>${qty}</td>
<td>₹${rate.toFixed(2)}</td>
<td>${discDisplay}</td>
<td>${sgst}%</td>
<td>${cgst}%</td>
<td>₹${total.toFixed(2)}</td>
</tr>
`;
});
itemsHtml += totalsRow;
document.getElementById('previewItems').innerHTML = itemsHtml;
document.getElementById('previewTotal').textContent = `₹${parseFloat(totalsRow.match(/₹([\d.]+)/)[1]).toFixed(2)}`;
}
// Generate PDF
async function generatePDF() {
try {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
const invoiceBox = document.querySelector('.invoice-box');
const companyName = document.getElementById('previewCompanyName').textContent;
const invoiceNumber = document.getElementById('previewInvoiceNumber').textContent;
// Add logo if present
const logo = document.querySelector('.invoice-logo img');
if (logo) {
const logoStyle = window.getComputedStyle(document.querySelector('.invoice-logo'));
const left = parseFloat(logoStyle.left) / 3.78; // Convert px to mm
const top = parseFloat(logoStyle.top) / 3.78;
const width = parseFloat(logoStyle.width) / 3.78;
const height = parseFloat(logoStyle.height) / 3.78;
const imgData = await getImageData(logo);
doc.addImage(imgData, 'PNG', left, top, width, height);
}
// Capture invoice content
const canvas = await html2canvas(invoiceBox, { scale: 2 });
const imgData = canvas.toDataURL('image/png');
doc.addImage(imgData, 'PNG', 10, 30, 190, 0);
doc.save(`${companyName}_Invoice_${invoiceNumber}.pdf`);
} catch (error) {
showStatus('Error generating PDF: ' + error.message, 'error');
}
}
// Share PDF via WhatsApp
// Replace your existing sharePDF() function with this improved version
async function sharePDF() {
try {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
const invoiceBox = document.querySelector('.invoice-box');
const companyName = document.getElementById('previewCompanyName').textContent;
const invoiceNumber = document.getElementById('previewInvoiceNumber').textContent;
const whatsappNumber = document.getElementById('whatsappNumber').value;
if (!whatsappNumber) {
showStatus('Please enter a WhatsApp number.', 'error');
return;
}
// Add logo if present
const logo = document.querySelector('.invoice-logo img');
if (logo) {
const logoStyle = window.getComputedStyle(document.querySelector('.invoice-logo'));
const left = parseFloat(logoStyle.left) / 3.78;
const top = parseFloat(logoStyle.top) / 3.78;
const width = parseFloat(logoStyle.width) / 3.78;
const height = parseFloat(logoStyle.height) / 3.78;
const imgData = await getImageData(logo);
doc.addImage(imgData, 'PNG', left, top, width, height);
}
// Capture the invoice with higher resolution
const canvas = await html2canvas(invoiceBox, {
scale: 2,
useCORS: true,
allowTaint: true,
height: invoiceBox.scrollHeight,
width: invoiceBox.scrollWidth
});
const imgData = canvas.toDataURL('image/png');
const imgWidth = 190; // PDF width in mm
const pageHeight = 277; // A4 page height in mm
const imgHeight = (canvas.height * imgWidth) / canvas.width;
let heightLeft = imgHeight;
let position = 30; // Start position
// Add first page
doc.addImage(imgData, 'PNG', 10, position, imgWidth, imgHeight);
heightLeft -= (pageHeight - position);
// Add additional pages if needed
while (heightLeft >= 0) {
position = heightLeft - imgHeight + 30;
doc.addPage();
doc.addImage(imgData, 'PNG', 10, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
const pdfBlob = doc.output('blob');
const file = new File([pdfBlob], `${companyName}_Invoice_${invoiceNumber}.pdf`, { type: 'application/pdf' });
if (navigator.canShare && navigator.canShare({ files: [file] })) {
await navigator.share({
files: [file],
title: `Invoice ${invoiceNumber}`,
text: `Invoice ${invoiceNumber} from ${companyName}`,
url: whatsappNumber ? `https://wa.me/${whatsappNumber.replace('+', '')}?text=Invoice%20${invoiceNumber}%20from%20${encodeURIComponent(companyName)}` : ''
});
} else {
// Fallback: Download the file and open WhatsApp
const url = URL.createObjectURL(pdfBlob);
const a = document.createElement('a');
a.href = url;
a.download = `${companyName}_Invoice_${invoiceNumber}.pdf`;
a.click();
URL.revokeObjectURL(url);
// Open WhatsApp with message
const whatsappUrl = `https://wa.me/${whatsappNumber.replace('+', '')}?text=Please%20find%20the%20invoice%20PDF%20attached.%20Invoice%20${invoiceNumber}%20from%20${encodeURIComponent(companyName)}`;
window.open(whatsappUrl, '_blank');
showStatus('PDF downloaded. Please manually attach it to WhatsApp.', 'success');
}
} catch (error) {
showStatus('Error sharing PDF: ' + error.message, 'error');
}
}
// Print invoice
function printInvoice() {
window.print();
}
// Get image data for PDF
function getImageData(img) {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
resolve(canvas.toDataURL('image/png'));
});
}
// Setup item autocomplete
function setupItemAutocomplete(row) {
const codeInput = row.querySelector('.item-code');
const dropdown = row.querySelector('.item-dropdown');
codeInput.addEventListener('input', debounce(async () => {
const query = codeInput.value.toLowerCase();
dropdown.innerHTML = '';
if (query.length < 1) {
dropdown.style.display = 'none';
return;
}
const filtered = inventory.filter(item =>
item.code.toLowerCase().includes(query) ||
item.description.toLowerCase().includes(query)
);
filtered.forEach(item => {
const div = document.createElement('div');
div.className = 'dropdown-item';
div.textContent = `${item.code} - ${item.description}`;
div.addEventListener('click', () => {
codeInput.value = item.code;
row.querySelector('.item-desc').value = item.description;
row.querySelector('.item-rate').value = item.price.toFixed(2);
row.querySelector('.item-unit').value = item.unit;
row.querySelector('.item-sgst').value = item.sgst;
row.querySelector('.item-cgst').value = item.cgst;
row.querySelector('.item-disc').value = item.discount;
row.querySelector('.disc-type-select').value = item.discountType;
[row.querySelector('.item-desc'), row.querySelector('.item-rate'), row.querySelector('.item-unit'),
row.querySelector('.item-sgst'), row.querySelector('.item-cgst'), row.querySelector('.item-disc'),
row.querySelector('.disc-type-select')].forEach(el => el.classList.add('auto-populated'));
updateItemTotal(row);
updateItemTotals();
dropdown.style.display = 'none';
});
dropdown.appendChild(div);
});
dropdown.style.display = filtered.length ? 'block' : 'none';
}, 300));
}
// Client contact autocomplete
function setupClientAutocomplete() {
const contactInput = document.getElementById('clientContact');
const dropdown = document.getElementById('contactDropdown');
contactInput.addEventListener('input', debounce(async () => {
const query = contactInput.value.toLowerCase();
dropdown.innerHTML = '';
if (query.length < 1) {
dropdown.style.display = 'none';
return;
}
const filtered = customerDatabase.filter(customer =>
customer.contact.toLowerCase().includes(query) ||
customer.name.toLowerCase().includes(query)
);
filtered.forEach(customer => {
const div = document.createElement('div');
div.className = 'dropdown-item';
div.textContent = `${customer.contact} - ${customer.name}`;
div.addEventListener('click', () => {
contactInput.value = customer.contact;
document.getElementById('clientName').value = customer.name;
document.getElementById('clientEmail').value = customer.email;
document.getElementById('clientAddress').value = customer.address;
document.getElementById('whatsappNumber').value = customer.contact;
[document.getElementById('clientName'), document.getElementById('clientEmail'),
document.getElementById('clientAddress'), document.getElementById('whatsappNumber')]
.forEach(el => el.classList.add('auto-populated'));
dropdown.style.display = 'none';
});
dropdown.appendChild(div);
});
dropdown.style.display = filtered.length ? 'block' : 'none';
}, 300));
}
// Load stock table
async function loadStockTable() {
try {
const stockTableBody = document.getElementById('stockTableBody');
stockTableBody.innerHTML = '';
inventory = await db.inventory.toArray();
inventory.forEach((item, index) => {
const row = document.createElement('tr');
const discDisplay = item.discountType === 'percent' ? `${item.discount}%` : `₹${item.discount.toFixed(2)}`;
row.innerHTML = `
<td>${sanitizeInput(item.code)}</td>
<td>${sanitizeInput(item.hsn)}</td>
<td>${sanitizeInput(item.description)}</td>
<td>₹${item.price.toFixed(2)}</td>
<td>${item.unit}</td>
<td>${item.sgst}%</td>
<td>${item.cgst}%</td>
<td>${discDisplay}</td>
<td class="${item.quantity < 10 ? 'low-stock' : ''}">${item.quantity}</td>
<td>
<button class="edit-btn" onclick="editStockItem(${index})">Edit</button>
<button class="delete-btn" onclick="deleteStockItem('${item.code}')">Delete</button>
</td>
`;
stockTableBody.appendChild(row);
});
} catch (error) {
showStatus('Error loading stock table: ' + error.message, 'error');
}
}
// Add stock item
async function addStockItem() {
try {
const item = {
code: sanitizeInput(document.getElementById('stockCode').value),
hsn: sanitizeInput(document.getElementById('stockHSN').value),
description: sanitizeInput(document.getElementById('stockDescription').value),
price: parseFloat(document.getElementById('stockPrice').value) || 0,
unit: document.getElementById('stockUnit').value,
quantity: parseInt(document.getElementById('stockQuantity').value) || 0,
sgst: parseFloat(document.getElementById('stockSGST').value) || 0,
cgst: parseFloat(document.getElementById('stockCGST').value) || 0,
discountType: document.getElementById('stockDiscountType').value,
discount: parseFloat(document.getElementById('stockDiscount').value) || 0
};
if (!item.code || !item.description || !item.hsn) {
showStatus('Please fill in all required fields (Code, HSN/SAC, Description).', 'error');
return;
}
if (inventory.some(i => i.code === item.code)) {
showStatus('Item code already exists.', 'error');
return;
}
await db.inventory.put(item);
inventory = await db.inventory.toArray();
loadStockTable();
document.getElementById('stockCode').value = '';
document.getElementById('stockHSN').value = '';
document.getElementById('stockDescription').value = '';
document.getElementById('stockPrice').value = '';
document.getElementById('stockUnit').value = 'PCS';
document.getElementById('stockQuantity').value = '';
document.getElementById('stockSGST').value = '9';
document.getElementById('stockCGST').value = '9';
document.getElementById('stockDiscountType').value = 'percent';
document.getElementById('stockDiscount').value = '0';
showStatus('Item added successfully!', 'success');
} catch (error) {
showStatus('Error adding stock item: ' + error.message, 'error');
}
}
// Edit stock item
function editStockItem(index) {
const item = inventory[index];
document.getElementById('stockCode').value = item.code;
document.getElementById('stockHSN').value = item.hsn;
document.getElementById('stockDescription').value = item.description;
document.getElementById('stockPrice').value = item.price;
document.getElementById('stockUnit').value = item.unit;
document.getElementById('stockQuantity').value = item.quantity;
document.getElementById('stockSGST').value = item.sgst;
document.getElementById('stockCGST').value = item.cgst;
document.getElementById('stockDiscountType').value = item.discountType;
document.getElementById('stockDiscount').value = item.discount;
document.getElementById('add-stock-btn').textContent = 'Update Item';
document.getElementById('add-stock-btn').onclick = async () => {
try {
const updatedItem = {
code: sanitizeInput(document.getElementById('stockCode').value),
hsn: sanitizeInput(document.getElementById('stockHSN').value),
description: sanitizeInput(document.getElementById('stockDescription').value),
price: parseFloat(document.getElementById('stockPrice').value) || 0,
unit: document.getElementById('stockUnit').value,
quantity: parseInt(document.getElementById('stockQuantity').value) || 0,
sgst: parseFloat(document.getElementById('stockSGST').value) || 0,
cgst: parseFloat(document.getElementById('stockCGST').value) || 0,
discountType: document.getElementById('stockDiscountType').value,
discount: parseFloat(document.getElementById('stockDiscount').value) || 0
};
if (!updatedItem.code || !updatedItem.description || !updatedItem.hsn) {
showStatus('Please fill in all required fields (Code, HSN/SAC, Description).', 'error');
return;
}
await db.inventory.put(updatedItem);
inventory = await db.inventory.toArray();
loadStockTable();
document.getElementById('add-stock-btn').textContent = 'Add to Inventory';
document.getElementById('add-stock-btn').onclick = addStockItem;
document.getElementById('stockCode').value = '';
document.getElementById('stockHSN').value = '';
document.getElementById('stockDescription').value = '';
document.getElementById('stockPrice').value = '';
document.getElementById('stockUnit').value = 'PCS';
document.getElementById('stockQuantity').value = '';
document.getElementById('stockSGST').value = '9';
document.getElementById('stockCGST').value = '9';
document.getElementById('stockDiscountType').value = 'percent';
document.getElementById('stockDiscount').value = '0';
showStatus('Item updated successfully!', 'success');
} catch (error) {
showStatus('Error updating stock item: ' + error.message, 'error');
}
};
}
// Delete stock item
async function deleteStockItem(code) {
if (confirm('Are you sure you want to delete this item?')) {
try {
await db.inventory.delete(code);
inventory = await db.inventory.toArray();
loadStockTable();
showStatus('Item deleted successfully!', 'success');
} catch (error) {
showStatus('Error deleting stock item: ' + error.message, 'error');
}
}
}
// Add customer
async function addCustomer() {
try {
const customer = {
contact: sanitizeInput(document.getElementById('newCustomerContact').value),
name: sanitizeInput(document.getElementById('newCustomerName').value),
email: sanitizeInput(document.getElementById('newCustomerEmail').value),
address: sanitizeInput(document.getElementById('newCustomerAddress').value)
};
if (!customer.contact || !customer.name) {
showStatus('Please fill in contact number and name.', 'error');
return;
}
if (customerDatabase.some(c => c.contact === customer.contact)) {
showStatus('Customer with this contact number already exists.', 'error');
return;
}
await db.customers.put(customer);
customerDatabase = await db.customers.toArray();
loadCustomerTable();
document.getElementById('newCustomerContact').value = '';
document.getElementById('newCustomerName').value = '';
document.getElementById('newCustomerEmail').value = '';
document.getElementById('newCustomerAddress').value = '';
showStatus('Customer added successfully!', 'success');
updateDashboard();
} catch (error) {
showStatus('Error adding customer: ' + error.message, 'error');
}
}
// Load customer table
async function loadCustomerTable() {
try {
const customerTableBody = document.getElementById('customerTableBody');
customerTableBody.innerHTML = '';
customerDatabase = await db.customers.toArray();
customerDatabase.forEach((customer, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${sanitizeInput(customer.contact)}</td>
<td>${sanitizeInput(customer.name)}</td>
<td>${sanitizeInput(customer.email)}</td>
<td>${sanitizeInput(customer.address)}</td>
<td>
<button class="edit-btn" onclick="editCustomer('${customer.contact}')">Edit</button>
<button class="delete-btn" onclick="deleteCustomer('${customer.contact}')">Delete</button>
</td>
`;
customerTableBody.appendChild(row);
});
} catch (error) {
showStatus('Error loading customer table: ' + error.message, 'error');
}
}
// Edit customer
function editCustomer(contact) {
const customer = customerDatabase.find(c => c.contact === contact);
if (!customer) return;
editingCustomerContact = contact;
document.getElementById('editCustomerContact').value = customer.contact;
document.getElementById('editCustomerName').value = customer.name;
document.getElementById('editCustomerEmail').value = customer.email;
document.getElementById('editCustomerAddress').value = customer.address;
document.getElementById('editCustomerDialogOverlay').style.display = 'flex';
}
// Save edited customer
async function saveEditedCustomer() {
try {
const updatedCustomer = {
contact: sanitizeInput(document.getElementById('editCustomerContact').value),
name: sanitizeInput(document.getElementById('editCustomerName').value),
email: sanitizeInput(document.getElementById('editCustomerEmail').value),
address: sanitizeInput(document.getElementById('editCustomerAddress').value)
};
if (!updatedCustomer.contact || !updatedCustomer.name) {
showStatus('Please fill in contact number and name.', 'error');
return;
}
if (updatedCustomer.contact !== editingCustomerContact && customerDatabase.some(c => c.contact === updatedCustomer.contact)) {
showStatus('Customer with this contact number already exists.', 'error');
return;
}
await db.customers.delete(editingCustomerContact);
await db.customers.put(updatedCustomer);
customerDatabase = await db.customers.toArray();
loadCustomerTable();
document.getElementById('editCustomerDialogOverlay').style.display = 'none';
showStatus('Customer updated successfully!', 'success');
editingCustomerContact = null;
} catch (error) {
showStatus('Error updating customer: ' + error.message, 'error');
}
}
// Delete customer
async function deleteCustomer(contact) {
if (confirm('Are you sure you want to delete this customer?')) {
try {
await db.customers.delete(contact);
customerDatabase = await db.customers.toArray();
loadCustomerTable();
showStatus('Customer deleted successfully!', 'success');
} catch (error) {
showStatus('Error deleting customer: ' + error.message, 'error');
}
}
}
// Load customer history
async function loadCustomerHistory() {
try {
const contact = document.getElementById('historyContact').value;
const historyTableBody = document.getElementById('customerHistoryTableBody');
historyTableBody.innerHTML = '';
const filteredInvoices = invoices.filter(inv => inv.clientContact === contact);
if (filteredInvoices.length === 0) {
showStatus('No invoices found for this customer.', 'error', 'historyStatusMessage');
return;
}
filteredInvoices.forEach(invoice => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${sanitizeInput(invoice.invoiceNumber)}</td>
<td>${invoice.invoiceDate}</td>
<td>${sanitizeInput(invoice.clientName)}</td>
<td>₹${invoice.grandTotal.toFixed(2)}</td>
<td>
<button class="toggle-items-btn" onclick="toggleInvoiceItems(this, '${invoice.invoiceNumber}')">Show Items</button>
</td>
`;
const itemsRow = document.createElement('tr');
itemsRow.className = 'items-details';
itemsRow.innerHTML = `
<td colspan="5">
<table class="item-table">
<thead>
<tr>
<th>Code</th>
<th>Description</th>
<th>Unit</th>
<th>Qty</th>
<th>Rate</th>
<th>Discount</th>
<th>SGST %</th>
<th>CGST %</th>
<th>Total</th>
</tr>
</thead>
<tbody>
${invoice.items.map(item => `
<tr>
<td>${sanitizeInput(item.code)}</td>
<td>${sanitizeInput(item.description)}</td>
<td>${item.unit}</td>
<td>${item.quantity}</td>
<td>₹${item.rate.toFixed(2)}</td>
<td>${item.discountType === 'percent' ? `${item.discount}%` : `₹${item.discount.toFixed(2)}`}</td>
<td>${item.sgst}%</td>
<td>${item.cgst}%</td>
<td>₹${item.total.toFixed(2)}</td>
</tr>
`).join('')}
</tbody>
</table>
</td>
`;
historyTableBody.appendChild(row);
historyTableBody.appendChild(itemsRow);
});
showStatus('Customer history loaded successfully!', 'success', 'historyStatusMessage');
} catch (error) {
showStatus('Error loading customer history: ' + error.message, 'error', 'historyStatusMessage');
}
}
// Toggle invoice items in customer history
function toggleInvoiceItems(button, invoiceNumber) {
const row = button.closest('tr');
const itemsRow = row.nextElementSibling;
if (itemsRow.classList.contains('active')) {
itemsRow.classList.remove('active');
button.textContent = 'Show Items';
} else {
itemsRow.classList.add('active');
button.textContent = 'Hide Items';
}
}
// Customer history contact autocomplete
function setupHistoryContactAutocomplete() {
const contactInput = document.getElementById('historyContact');
const dropdown = document.getElementById('historyContactDropdown');
contactInput.addEventListener('input', debounce(async () => {
const query = contactInput.value.toLowerCase();
dropdown.innerHTML = '';
if (query.length < 1) {
dropdown.style.display = 'none';
return;
}
const filtered = customerDatabase.filter(customer =>
customer.contact.toLowerCase().includes(query) ||
customer.name.toLowerCase().includes(query)
);
filtered.forEach(customer => {
const div = document.createElement('div');
div.className = 'dropdown-item';
div.textContent = `${customer.contact} - ${customer.name}`;
div.addEventListener('click', () => {
contactInput.value = customer.contact;
dropdown.style.display = 'none';
loadCustomerHistory();
});
dropdown.appendChild(div);
});
dropdown.style.display = filtered.length ? 'block' : 'none';
}, 300));
}
// Open item dialog
async function openItemDialog() {
try {
const itemsList = document.getElementById('itemsList2');
itemsList.innerHTML = '';
filteredInventory = await db.inventory.toArray();
filteredInventory.forEach(item => {
const discDisplay = item.discountType === 'percent' ? `${item.discount}%` : `₹${item.discount.toFixed(2)}`;
const row = document.createElement('tr');
row.innerHTML = `
<td>${sanitizeInput(item.code)}</td>
<td>${sanitizeInput(item.name)}</td>
<td>${sanitizeInput(item.description)}</td>
<td>₹${item.price.toFixed(2)}</td>
<td>${item.sgst}%</td>
<td>${item.cgst}%</td>
<td>${discDisplay}</td>
`;
row.addEventListener('click', () => {
const newRow = addItem();
newRow.querySelector('.item-code').value = item.code;
newRow.querySelector('.item-desc').value = item.description;
newRow.querySelector('.item-rate').value = item.price.toFixed(2);
newRow.querySelector('.item-unit').value = item.unit;
newRow.querySelector('.item-sgst').value = item.sgst;
newRow.querySelector('.item-cgst').value = item.cgst;
newRow.querySelector('.item-disc').value = item.discount;
newRow.querySelector('.disc-type-select').value = item.discountType;
[newRow.querySelector('.item-desc'), newRow.querySelector('.item-rate'), newRow.querySelector('.item-unit'),
newRow.querySelector('.item-sgst'), newRow.querySelector('.item-cgst'), newRow.querySelector('.item-disc'),
newRow.querySelector('.disc-type-select')].forEach(el => el.classList.add('auto-populated'));
updateItemTotal(newRow);
updateItemTotals();
});
itemsList.appendChild(row);
});
document.getElementById('dialogOverlay').style.display = 'flex';
} catch (error) {
showStatus('Error opening item dialog: ' + error.message, 'error');
}
}
// Search items in dialog
async function searchItems() {
try {
const query = document.getElementById('searchInput').value.toLowerCase();
const itemsList = document.getElementById('itemsList2');
itemsList.innerHTML = '';
filteredInventory = inventory.filter(item =>
item.code.toLowerCase().includes(query) ||
item.description.toLowerCase().includes(query)
);
filteredInventory.forEach(item => {
const discDisplay = item.discountType === 'percent' ? `${item.discount}%` : `₹${item.discount.toFixed(2)}`;
const row = document.createElement('tr');
row.innerHTML = `
<td>${sanitizeInput(item.code)}</td>
<td>${sanitizeInput(item.name)}</td>
<td>${sanitizeInput(item.description)}</td>
<td>₹${item.price.toFixed(2)}</td>
<td>${item.sgst}%</td>
<td>${item.cgst}%</td>
<td>${discDisplay}</td>
`;
row.addEventListener('click', () => {
const newRow = addItem();
newRow.querySelector('.item-code').value = item.code;
newRow.querySelector('.item-desc').value = item.description;
newRow.querySelector('.item-rate').value = item.price.toFixed(2);
newRow.querySelector('.item-unit').value = item.unit;
newRow.querySelector('.item-sgst').value = item.sgst;
newRow.querySelector('.item-cgst').value = item.cgst;
newRow.querySelector('.item-disc').value = item.discount;
newRow.querySelector('.disc-type-select').value = item.discountType;
[newRow.querySelector('.item-desc'), newRow.querySelector('.item-rate'), newRow.querySelector('.item-unit'),
newRow.querySelector('.item-sgst'), newRow.querySelector('.item-cgst'), newRow.querySelector('.item-disc'),
newRow.querySelector('.disc-type-select')].forEach(el => el.classList.add('auto-populated'));
updateItemTotal(newRow);
updateItemTotals();
document.getElementById('dialogOverlay').style.display = 'none';
});
itemsList.appendChild(row);
});
} catch (error) {
showStatus('Error searching items: ' + error.message, 'error');
}
}
// Close dialog
function closeDialog(event, overlayId) {
if (event && event.target.id !== overlayId) return;
document.getElementById(overlayId).style.display = 'none';
}
// Apply custom dialog size
function applyCustomSize(dialogId, widthInputId, heightInputId) {
const dialog = document.getElementById(dialogId);
const width = parseInt(document.getElementById(widthInputId).value);
const height = parseInt(document.getElementById(heightInputId).value);
if (width >= 300 && width <= 1200 && height >= 200 && height <= 800) {
dialog.style.width = `${width}px`;
dialog.style.height = `${height}px`;
} else {
showStatus('Width must be between 300-1200px and height between 200-800px.', 'error');
}
}
// Quick resize dialog
function quickResize(width, height, dialogId, widthInputId, heightInputId) {
document.getElementById(dialogId).style.width = `${width}px`;
document.getElementById(dialogId).style.height = `${height}px`;
document.getElementById(widthInputId).value = width;
document.getElementById(heightInputId).value = height;
}
// Backup all data
async function backupData() {
try {
const data = {
inventory: await db.inventory.toArray(),
customers: await db.customers.toArray(),
invoices: await db.invoices.toArray()
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `invoice_backup_${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
showStatus('Backup created successfully!', 'success', 'backupStatusMessage');
} catch (error) {
showStatus('Error creating backup: ' + error.message, 'error', 'backupStatusMessage');
}
}
// Restore all data
async function restoreData() {
try {
const fileInput = document.getElementById('restoreFileInput');
const file = fileInput.files[0];
if (!file) {
showStatus('Please select a backup file.', 'error', 'backupStatusMessage');
return;
}
const text = await file.text();
const data = JSON.parse(text);
await db.inventory.clear();
await db.customers.clear();
await db.invoices.clear();
if (data.inventory) await db.inventory.bulkPut(data.inventory);
if (data.customers) await db.customers.bulkPut(data.customers);
if (data.invoices) await db.invoices.bulkPut(data.invoices);
await loadDataFromIndexedDB();
loadStockTable();
loadCustomerTable();
updateDashboard();
showStatus('Data restored successfully!', 'success', 'backupStatusMessage');
fileInput.value = '';
} catch (error) {
showStatus('Error restoring data: ' + error.message, 'error', 'backupStatusMessage');
}
}
// Backup stock data
async function backupStockData() {
try {
const data = await db.inventory.toArray();
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `stock_backup_${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
showStatus('Stock backup created successfully!', 'success', 'backupStatusMessage');
} catch (error) {
showStatus('Error creating stock backup: ' + error.message, 'error', 'backupStatusMessage');
}
}
// Restore stock data
async function restoreStockData() {
try {
const fileInput = document.getElementById('restoreStockFileInput');
const file = fileInput.files[0];
if (!file) {
showStatus('Please select a stock backup file.', 'error', 'backupStatusMessage');
return;
}
const text = await file.text();
const data = JSON.parse(text);
await db.inventory.clear();
await db.inventory.bulkPut(data);
inventory = await db.inventory.toArray();
loadStockTable();
showStatus('Stock data restored successfully!', 'success', 'backupStatusMessage');
fileInput.value = '';
} catch (error) {
showStatus('Error restoring stock data: ' + error.message, 'error', 'backupStatusMessage');
}
}
// Backup customer data
async function backupCustomerData() {
try {
const data = await db.customers.toArray();
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `customer_backup_${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
showStatus('Customer backup created successfully!', 'success', 'backupStatusMessage');
} catch (error) {
showStatus('Error creating customer backup: ' + error.message, 'error', 'backupStatusMessage');
}
}
// Restore customer data
async function restoreCustomerData() {
try {
const fileInput = document.getElementById('restoreCustomerFileInput');
const file = fileInput.files[0];
if (!file) {
showStatus('Please select a customer backup file.', 'error', 'backupStatusMessage');
return;
}
const text = await file.text();
const data = JSON.parse(text);
await db.customers.clear();
await db.customers.bulkPut(data);
customerDatabase = await db.customers.toArray();
loadCustomerTable();
showStatus('Customer data restored successfully!', 'success', 'backupStatusMessage');
fileInput.value = '';
} catch (error) {
showStatus('Error restoring customer data: ' + error.message, 'error', 'backupStatusMessage');
}
}
// Export invoices to CSV
function exportInvoicesToCSV() {
if (invoices.length === 0) {
showStatus('No invoices to export.', 'error');
return;
}
let csv = 'Invoice Number,Invoice Date,Due Date,Client Name,Client Contact,Client Email,Client Address,Item Code,Item Description,Item Unit,Item Quantity,Item Rate,Item Discount Type,Item Discount,Item SGST,Item CGST,Item Total,Invoice Subtotal,Invoice Total Discount,Invoice Total SGST,Invoice Total CGST,Invoice Grand Total\n';
invoices.forEach(invoice => {
invoice.items.forEach(item => {
csv += `"${invoice.invoiceNumber}","${invoice.invoiceDate}","${invoice.dueDate}","${invoice.clientName.replace(/"/g, '""')}","${invoice.clientContact.replace(/"/g, '""')}","${invoice.clientEmail.replace(/"/g, '""')}","${invoice.clientAddress.replace(/"/g, '""')}","${item.code.replace(/"/g, '""')}","${item.description.replace(/"/g, '""')}","${item.unit}","${item.quantity}","${item.rate.toFixed(2)}","${item.discountType}","${item.discount.toFixed(2)}","${item.sgst.toFixed(2)}","${item.cgst.toFixed(2)}","${item.total.toFixed(2)}","${invoice.subtotal.toFixed(2)}","${invoice.totalDiscount.toFixed(2)}","${invoice.totalSGST.toFixed(2)}","${invoice.totalCGST.toFixed(2)}","${invoice.grandTotal.toFixed(2)}"\n`;
});
});
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `invoices_${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
showStatus('Invoices exported to CSV successfully!', 'success');
}
// Generate WhatsApp message
function generateWhatsAppMessage() {
const whatsappNumber = document.getElementById('whatsappNumber').value;
const maxDescLength = parseInt(document.getElementById('whatsappDescLength').value) || 20;
const gstEnabled = document.getElementById('gstEnabled').checked;
const companyName = document.getElementById('companyName').value || 'Your Company Name';
const invoiceNumber = document.getElementById('invoiceNumber').value;
const clientName = document.getElementById('clientName').value;
const invoiceDate = document.getElementById('invoiceDate').value;
let message = `Invoice from *${companyName}*\n`;
message += `Invoice #: ${invoiceNumber}\n`;
message += `Date: ${invoiceDate}\n`;
message += `To: ${clientName}\n\n`;
message += `Items:\n`;
let grandTotal = 0;
let totalDiscount = 0;
document.querySelectorAll('.item-row').forEach(row => {
const desc = row.querySelector('.item-desc').value;
const qty = parseFloat(row.querySelector('.item-qty').value) || 0;
const rate = parseFloat(row.querySelector('.item-rate').value) || 0;
const discType = row.querySelector('.disc-type-select').value;
const disc = parseFloat(row.querySelector('.item-disc').value) || 0;
const sgst = parseFloat(row.querySelector('.item-sgst').value) || 0;
const cgst = parseFloat(row.querySelector('.item-cgst').value) || 0;
const subtotalAmount = qty * rate;
const discAmount = discType === 'percent' ? subtotalAmount * (disc / 100) : disc;
const afterDisc = subtotalAmount - discAmount;
const sgstAmount = afterDisc * (sgst / 100);
const cgstAmount = afterDisc * (cgst / 100);
const lineTotal = afterDisc + sgstAmount + cgstAmount;
totalDiscount += discAmount;
const shortDesc = desc.length > maxDescLength ? desc.substring(0, maxDescLength) + '...' : desc;
message += `- ${shortDesc} (Qty: ${qty}, Rate: ₹${rate.toFixed(2)})`;
message += `, Discount: ${discType === 'percent' ? `${disc}%` : `₹${disc.toFixed(2)}`}`; // Add discount
if (gstEnabled) {
message += `, SGST: ${sgst}%, CGST: ${cgst}%`;
}
message += ` = ₹${lineTotal.toFixed(2)}\n`;
grandTotal += lineTotal;
});
message += `\nTotal Discount: ₹${totalDiscount.toFixed(2)}\n`; // Add total discount
message += `Grand Total: ₹${grandTotal.toFixed(2)}\n`;
message += `Thank you for your business!`;
const preview = document.getElementById('whatsappPreview');
preview.textContent = message;
preview.style.display = 'block';
}
// Send WhatsApp message
function sendWhatsApp() {
const whatsappNumber = document.getElementById('whatsappNumber').value;
const preview = document.getElementById('whatsappPreview').textContent;
if (!whatsappNumber) {
showStatus('Please enter a WhatsApp number.', 'error');
return;
}
const encodedMessage = encodeURIComponent(preview);
const url = `https://wa.me/${whatsappNumber.replace('+', '')}?text=${encodedMessage}`;
window.open(url, '_blank');
}
// Logo handling
let isDragging = false, isResizing = false, currentHandle = null;
let startX, startY, startWidth, startHeight, startLeft, startTop;
function setupLogoHandling() {
const logoUpload = document.getElementById('logoUpload');
const logoWidthInput = document.getElementById('logoWidth');
const logoHeightInput = document.getElementById('logoHeight');
const lockAspectRatio = document.getElementById('lockAspectRatio');
logoUpload.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.src = event.target.result;
img.onload = () => {
const logoContainer = document.createElement('div');
logoContainer.className = 'invoice-logo';
logoContainer.style.width = '100px';
logoContainer.style.height = '100px';
logoContainer.style.left = '20px';
logoContainer.style.top = '20px';
logoContainer.innerHTML = `
<img src="${img.src}" alt="Logo">
<div class="resize-handle top-left"></div>
<div class="resize-handle top-right"></div>
<div class="resize-handle bottom-left"></div>
<div class="resize-handle bottom-right"></div>
`;
document.querySelector('.invoice-box').prepend(logoContainer);
logoWidthInput.value = 100;
logoHeightInput.value = 100;
setupLogoInteractions(logoContainer, img);
};
};
reader.readAsDataURL(file);
}
});
logoWidthInput.addEventListener('input', () => {
const logo = document.querySelector('.invoice-logo');
if (logo) {
let width = parseInt(logoWidthInput.value) || 100;
if (lockAspectRatio.checked) {
const img = logo.querySelector('img');
const aspectRatio = img.naturalWidth / img.naturalHeight;
logoHeightInput.value = Math.round(width / aspectRatio);
logo.style.height = `${logoHeightInput.value}px`;
}
logo.style.width = `${width}px`;
}
});
logoHeightInput.addEventListener('input', () => {
const logo = document.querySelector('.invoice-logo');
if (logo) {
let height = parseInt(logoHeightInput.value) || 100;
if (lockAspectRatio.checked) {
const img = logo.querySelector('img');
const aspectRatio = img.naturalWidth / img.naturalHeight;
logoWidthInput.value = Math.round(height * aspectRatio);
logo.style.width = `${logoWidthInput.value}px`;
}
logo.style.height = `${height}px`;
}
});
}
function setupLogoInteractions(logoContainer, img) {
const logoWidthInput = document.getElementById('logoWidth');
const logoHeightInput = document.getElementById('logoHeight');
const lockAspectRatio = document.getElementById('lockAspectRatio');
logoContainer.addEventListener('mousedown', (e) => {
if (e.target.classList.contains('resize-handle')) {
isResizing = true;
currentHandle = e.target.className.split(' ')[1];
startX = e.clientX;
startY = e.clientY;
startWidth = parseFloat(logoContainer.style.width);
startHeight = parseFloat(logoContainer.style.height);
startLeft = parseFloat(logoContainer.style.left);
startTop = parseFloat(logoContainer.style.top);
} else {
isDragging = true;
startX = e.clientX - parseFloat(logoContainer.style.left);
startY = e.clientY - parseFloat(logoContainer.style.top);
logoContainer.classList.add('active');
}
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
logoContainer.style.left = `${e.clientX - startX}px`;
logoContainer.style.top = `${e.clientY - startY}px`;
} else if (isResizing) {
let newWidth, newHeight, newLeft = startLeft, newTop = startTop;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
const aspectRatio = img.naturalWidth / img.naturalHeight;
if (currentHandle.includes('top-left')) {
newWidth = startWidth - deltaX;
newHeight = lockAspectRatio.checked ? newWidth / aspectRatio : startHeight - deltaY;
newLeft = startLeft + deltaX;
newTop = lockAspectRatio.checked ? startTop + deltaX / aspectRatio : startTop + deltaY;
} else if (currentHandle.includes('top-right')) {
newWidth = startWidth + deltaX;
newHeight = lockAspectRatio.checked ? newWidth / aspectRatio : startHeight - deltaY;
newTop = lockAspectRatio.checked ? startTop + deltaX / aspectRatio : startTop + deltaY;
} else if (currentHandle.includes('bottom-left')) {
newWidth = startWidth - deltaX;
newHeight = lockAspectRatio.checked ? newWidth / aspectRatio : startHeight + deltaY;
newLeft = startLeft + deltaX;
} else if (currentHandle.includes('bottom-right')) {
newWidth = startWidth + deltaX;
newHeight = lockAspectRatio.checked ? newWidth / aspectRatio : startHeight + deltaY;
}
if (newWidth >= 20 && newWidth <= 300 && newHeight >= 20 && newHeight <= 300) {
logoContainer.style.width = `${newWidth}px`;
logoContainer.style.height = `${newHeight}px`;
logoContainer.style.left = `${newLeft}px`;
logoContainer.style.top = `${newTop}px`;
logoWidthInput.value = Math.round(newWidth);
logoHeightInput.value = Math.round(newHeight);
}
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
isResizing = false;
currentHandle = null;
});
logoContainer.addEventListener('dblclick', () => {
logoContainer.classList.toggle('active');
});
logoContainer.addEventListener('touchstart', (e) => {
const touch = e.touches[0];
if (e.target.classList.contains('resize-handle')) {
isResizing = true;
currentHandle = e.target.className.split(' ')[1];
startX = touch.clientX;
startY = touch.clientY;
startWidth = parseFloat(logoContainer.style.width);
startHeight = parseFloat(logoContainer.style.height);
startLeft = parseFloat(logoContainer.style.left);
startTop = parseFloat(logoContainer.style.top);
} else {
isDragging = true;
startX = touch.clientX - parseFloat(logoContainer.style.left);
startY = touch.clientY - parseFloat(logoContainer.style.top);
logoContainer.classList.add('active');
}
e.preventDefault();
});
document.addEventListener('touchmove', (e) => {
const touch = e.touches[0];
if (isDragging) {
logoContainer.style.left = `${touch.clientX - startX}px`;
logoContainer.style.top = `${touch.clientY - startY}px`;
} else if (isResizing) {
let newWidth, newHeight, newLeft = startLeft, newTop = startTop;
const deltaX = touch.clientX - startX;
const deltaY = touch.clientY - startY;
const aspectRatio = img.naturalWidth / img.naturalHeight;
if (currentHandle.includes('top-left')) {
newWidth = startWidth - deltaX;
newHeight = lockAspectRatio.checked ? newWidth / aspectRatio : startHeight - deltaY;
newLeft = startLeft + deltaX;
newTop = lockAspectRatio.checked ? startTop + deltaX / aspectRatio : startTop + deltaY;
} else if (currentHandle.includes('top-right')) {
newWidth = startWidth + deltaX;
newHeight = lockAspectRatio.checked ? newWidth / aspectRatio : startHeight - deltaY;
newTop = lockAspectRatio.checked ? startTop + deltaX / aspectRatio : startTop + deltaY;
} else if (currentHandle.includes('bottom-left')) {
newWidth = startWidth - deltaX;
newHeight = lockAspectRatio.checked ? newWidth / aspectRatio : startHeight + deltaY;
newLeft = startLeft + deltaX;
} else if (currentHandle.includes('bottom-right')) {
newWidth = startWidth + deltaX;
newHeight = lockAspectRatio.checked ? newWidth / aspectRatio : startHeight + deltaY;
}
if (newWidth >= 20 && newWidth <= 300 && newHeight >= 20 && newHeight <= 300) {
logoContainer.style.width = `${newWidth}px`;
logoContainer.style.height = `${newHeight}px`;
logoContainer.style.left = `${newLeft}px`;
logoContainer.style.top = `${newTop}px`;
logoWidthInput.value = Math.round(newWidth);
logoHeightInput.value = Math.round(newHeight);
}
}
});
document.addEventListener('touchend', () => {
isDragging = false;
isResizing = false;
currentHandle = null;
});
}
function resetLogoPosition() {
const logo = document.querySelector('.invoice-logo');
if (logo) {
logo.style.left = '20px';
logo.style.top = '20px';
logo.style.width = '100px';
logo.style.height = '100px';
document.getElementById('logoWidth').value = 100;
document.getElementById('logoHeight').value = 100;
}
}
function removeLogo() {
const logo = document.querySelector('.invoice-logo');
if (logo) logo.remove();
document.getElementById('logoUpload').value = '';
document.getElementById('logoWidth').value = '';
document.getElementById('logoHeight').value = '';
}
// Update dashboard
async function updateDashboard() {
try {
const dateRange = document.getElementById('dateRangeFilter').value;
let fromDate, toDate = new Date();
if (dateRange === 'custom') {
fromDate = new Date(document.getElementById('fromDate').value);
toDate = new Date(document.getElementById('toDate').value);
} else {
fromDate = new Date();
fromDate.setDate(toDate.getDate() - parseInt(dateRange));
}
const filteredInvoices = invoices.filter(inv => {
const invDate = new Date(inv.timestamp);
return invDate >= fromDate && invDate <= toDate;
});
let totalRevenue = 0, totalOrders = filteredInvoices.length, totalProducts = 0, totalDiscount = 0;
const productSales = {};
filteredInvoices.forEach(invoice => {
totalRevenue += invoice.grandTotal;
totalDiscount += invoice.totalDiscount;
invoice.items.forEach(item => {
totalProducts += item.quantity;
productSales[item.code] = (productSales[item.code] || 0) + item.quantity;
});
});
const uniqueCustomers = new Set(filteredInvoices.map(inv => inv.clientContact)).size;
document.getElementById('totalRevenue').textContent = `₹${totalRevenue.toFixed(2)}`;
document.getElementById('totalOrders').textContent = totalOrders;
document.getElementById('totalCustomers').textContent = uniqueCustomers;
document.getElementById('totalProducts').textContent = totalProducts;
document.getElementById('totalDiscount').textContent = `₹${totalDiscount.toFixed(2)}`;
const topProductsList = document.getElementById('topProductsList');
topProductsList.innerHTML = '';
const sortedProducts = Object.entries(productSales)
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
sortedProducts.forEach(([code, qty]) => {
const item = inventory.find(i => i.code === code);
const div = document.createElement('div');
div.textContent = `${item ? item.description : code}: ${qty} units`;
div.addEventListener('click', () => {
document.getElementById('stockCode').value = code;
document.getElementById('nav-btn-stock').click();
});
topProductsList.appendChild(div);
});
const recentOrdersList = document.getElementById('recentOrdersList');
recentOrdersList.innerHTML = '';
filteredInvoices.slice(-5).reverse().forEach(invoice => {
const div = document.createElement('div');
div.textContent = `${invoice.invoiceNumber} - ${invoice.clientName}: ₹${invoice.grandTotal.toFixed(2)}`;
div.addEventListener('click', () => {
document.getElementById('historyContact').value = invoice.clientContact;
document.getElementById('nav-btn-customer-history').click();
loadCustomerHistory();
});
recentOrdersList.appendChild(div);
});
} catch (error) {
showStatus('Error updating dashboard: ' + error.message, 'error');
}
}
// Toggle custom date range
function toggleCustomDateRange() {
const dateRange = document.getElementById('dateRangeFilter').value;
const customDateRange = document.getElementById('customDateRange');
customDateRange.classList.toggle('hidden', dateRange !== 'custom');
updateDashboard();
}
// Clear invoices
async function clearInvoices() {
if (confirm('Are you sure you want to clear all invoices? This action cannot be undone.')) {
try {
await db.invoices.clear();
invoices = [];
updateDashboard();
showStatus('All invoices cleared successfully!', 'success');
} catch (error) {
showStatus('Error clearing invoices: ' + error.message, 'error');
}
}
}
// Initialize application
async function init() {
try {
await migrateLocalStorageToIndexedDB();
await loadDataFromIndexedDB();
setupNavigation();
setupClientAutocomplete();
setupHistoryContactAutocomplete();
setupLogoHandling();
document.querySelectorAll('.item-row').forEach(row => {
attachInputListeners(row);
setupItemAutocomplete(row);
});
document.getElementById('invoiceDate').value = new Date().toISOString().split('T')[0];
const dueDate = new Date();
dueDate.setDate(dueDate.getDate() + 30);
document.getElementById('dueDate').value = dueDate.toISOString().split('T')[0];
document.getElementById('invoiceNumber').value = await generateInvoiceNumber();
loadStockTable();
loadCustomerTable();
updateDashboard();
updateItemTotals();
} catch (error) {
showStatus('Error initializing application: ' + error.message, 'error');
}
}
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
No comments:
Post a Comment