| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>FMCG OrderFlow Pro - Modern Order Management</title> |
| <link rel="icon" type="image/x-icon" href="/static/favicon.ico"> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/animejs/lib/anime.iife.min.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> |
| <script src="https://unpkg.com/feather-icons"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); |
| |
| body { |
| font-family: 'Inter', sans-serif; |
| } |
| |
| .glass-effect { |
| backdrop-filter: blur(10px); |
| background: rgba(255, 255, 255, 0.1); |
| border: 1px solid rgba(255, 255, 255, 0.2); |
| } |
| |
| .dark .glass-effect { |
| background: rgba(0, 0, 0, 0.3); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| } |
| |
| .gradient-bg { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| } |
| |
| .dark .gradient-bg { |
| background: linear-gradient(135deg, #4c51bf 0%, #553c9a 100%); |
| } |
| |
| .hover-scale { |
| transition: transform 0.2s ease-in-out; |
| } |
| |
| .hover-scale:hover { |
| transform: scale(1.02); |
| } |
| |
| .animate-float { |
| animation: float 3s ease-in-out infinite; |
| } |
| |
| @keyframes float { |
| 0%, 100% { transform: translateY(0px); } |
| 50% { transform: translateY(-10px); } |
| } |
| |
| .custom-scrollbar::-webkit-scrollbar { |
| width: 6px; |
| } |
| |
| .custom-scrollbar::-webkit-scrollbar-track { |
| background: rgba(0, 0, 0, 0.1); |
| border-radius: 3px; |
| } |
| |
| .custom-scrollbar::-webkit-scrollbar-thumb { |
| background: rgba(0, 0, 0, 0.3); |
| border-radius: 3px; |
| } |
| |
| .dark .custom-scrollbar::-webkit-scrollbar-track { |
| background: rgba(255, 255, 255, 0.1); |
| } |
| |
| .dark .custom-scrollbar::-webkit-scrollbar-thumb { |
| background: rgba(255, 255, 255, 0.3); |
| } |
| |
| .pulse-animation { |
| animation: pulse 2s infinite; |
| } |
| |
| @keyframes pulse { |
| 0% { opacity: 1; } |
| 50% { opacity: 0.7; } |
| 100% { opacity: 1; } |
| } |
| |
| .slide-in { |
| animation: slideIn 0.3s ease-out; |
| } |
| |
| @keyframes slideIn { |
| from { transform: translateY(-20px); opacity: 0; } |
| to { transform: translateY(0); opacity: 1; } |
| } |
| |
| .fade-in { |
| animation: fadeIn 0.5s ease-out; |
| } |
| |
| @keyframes fadeIn { |
| from { opacity: 0; } |
| to { opacity: 1; } |
| } |
| |
| .customer-dropdown { |
| max-height: 300px; |
| overflow-y: auto; |
| } |
| |
| .product-card { |
| transition: all 0.3s ease; |
| } |
| |
| .product-card:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 10px 25px rgba(0,0,0,0.1); |
| } |
| |
| .dark .product-card:hover { |
| box-shadow: 0 10px 25px rgba(0,0,0,0.3); |
| } |
| |
| .order-item { |
| transition: all 0.2s ease; |
| } |
| |
| .order-item:hover { |
| background-color: rgba(59, 130, 246, 0.1); |
| } |
| |
| .dark .order-item:hover { |
| background-color: rgba(59, 130, 246, 0.2); |
| } |
| |
| .tab-active { |
| background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); |
| color: white; |
| box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3); |
| } |
| |
| .dark .tab-active { |
| background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%); |
| } |
| |
| .print-header { |
| font-size: 14px; |
| font-weight: bold; |
| text-align: center; |
| } |
| |
| .print-body { |
| width: 80mm; |
| font-family: 'Courier New', monospace; |
| font-size: 11px; |
| margin: 5mm; |
| } |
| |
| .print-line { |
| border-top: 1px dashed #000; |
| margin: 5px 0; |
| } |
| |
| .print-table { |
| width: 100%; |
| border-collapse: collapse; |
| } |
| |
| .print-table td { |
| padding: 2px 0; |
| } |
| |
| .print-right { |
| text-align: right; |
| } |
| |
| .print-center { |
| text-align: center; |
| } |
| |
| .print-bold { |
| font-weight: bold; |
| } |
| </style> |
| </head> |
| <body class="bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 transition-colors duration-300"> |
| <div id="app"> |
| |
| <div class="fixed inset-0 bg-gradient-to-br from-blue-600 to-purple-700 flex items-center justify-center z-50" id="loading-screen"> |
| <div class="text-center text-white"> |
| <div class="animate-float mb-4"> |
| <i data-feather="shopping-cart" class="w-16 h-16 mx-auto"></i> |
| </div> |
| <h1 class="text-3xl font-bold mb-2">FMCG OrderFlow Pro</h1> |
| <p class="text-blue-100">Loading your order management system...</p> |
| <div class="mt-4 flex justify-center"> |
| <div class="w-2 h-2 bg-white rounded-full mx-1 pulse-animation"></div> |
| <div class="w-2 h-2 bg-white rounded-full mx-1 pulse-animation" style="animation-delay: 0.2s;"></div> |
| <div class="w-2 h-2 bg-white rounded-full mx-1 pulse-animation" style="animation-delay: 0.4s;"></div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="min-h-screen transition-colors duration-300" id="main-app" style="display: none;"> |
| |
| <header class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-md shadow-lg border-b border-white/20 dark:border-gray-700/20 sticky top-0 z-40"> |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> |
| <div class="flex justify-between items-center py-4"> |
| <div class="flex items-center space-x-4"> |
| <div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg hover-scale"> |
| <i data-feather="shopping-bag" class="w-6 h-6 text-white"></i> |
| </div> |
| <div> |
| <h1 class="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">FMCG OrderFlow Pro</h1> |
| <p class="text-sm text-gray-500 dark:text-gray-400">Modern Order Management</p> |
| </div> |
| </div> |
| <div class="flex items-center space-x-4"> |
| <div class="hidden sm:flex items-center text-sm text-gray-600 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 px-3 py-1 rounded-full"> |
| <i data-feather="user" class="w-4 h-4 mr-2"></i> |
| <span id="owner-name">Rajesh Kumar</span> |
| </div> |
| <button id="theme-toggle" class="p-2 rounded-lg bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"> |
| <i data-feather="sun" class="w-5 h-5 text-yellow-500 dark:hidden"></i> |
| <i data-feather="moon" class="w-5 h-5 text-blue-400 hidden dark:block"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| </header> |
|
|
| |
| <nav class="bg-white/70 dark:bg-gray-800/70 backdrop-blur-md shadow-sm border-b border-white/30 dark:border-gray-700/30 sticky top-16 z-30"> |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> |
| <div class="flex space-x-1 sm:space-x-4 overflow-x-auto py-2" id="nav-tabs"> |
| |
| </div> |
| </div> |
| </nav> |
|
|
| |
| <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> |
| <div id="content-area"> |
| |
| </div> |
| </main> |
|
|
| |
| <div id="modal-container"></div> |
| </div> |
| </div> |
|
|
| <script> |
| // Global state management |
| class AppState { |
| constructor() { |
| this.state = { |
| activeTab: 'order', |
| theme: 'light', |
| products: [], |
| customers: [], |
| currentOrder: [], |
| selectedCustomer: null, |
| searchTerm: '', |
| customerSearch: '', |
| showCustomerDropdown: false, |
| orders: [], |
| startDate: '', |
| endDate: '', |
| invoiceNumber: 1001, |
| shopProfile: { |
| shopName: 'YOUR COMPANY NAME', |
| ownerName: 'Rajesh Kumar', |
| address: 'Address Line 1, City - PIN', |
| phone: '+91-XXXXXXXXXX', |
| email: 'info@yourcompany.com', |
| gstin: '27AABCCDDEEFFG' |
| } |
| }; |
| this.listeners = []; |
| } |
| |
| getState() { |
| return this.state; |
| } |
| |
| setState(newState) { |
| this.state = { ...this.state, ...newState }; |
| this.notifyListeners(); |
| } |
| |
| subscribe(listener) { |
| this.listeners.push(listener); |
| } |
| |
| notifyListeners() { |
| this.listeners.forEach(listener => listener(this.state)); |
| } |
| } |
| |
| // Initialize app state |
| const appState = new AppState(); |
| |
| // UI Components |
| class UIComponents { |
| static createProductCard(product, onAddToCart) { |
| return ` |
| <div class="product-card border border-gray-200 dark:border-gray-700 rounded-xl p-4 hover:shadow-lg transition-all duration-300 cursor-pointer bg-white dark:bg-gray-800 hover:bg-blue-50 dark:hover:bg-blue-900/20 hover-scale" onclick="${onAddToCart}"> |
| <div class="flex justify-between items-start mb-2"> |
| <h3 class="font-semibold text-gray-800 dark:text-white text-sm">${product.name}</h3> |
| <span class="text-xs bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400 px-2 py-1 rounded-full">${product.category}</span> |
| </div> |
| <p class="text-xs text-gray-500 dark:text-gray-400 mb-2">Code: ${product.code}</p> |
| <div class="flex justify-between items-center"> |
| <span class="text-lg font-bold text-blue-600 dark:text-blue-400">₹${product.price.toFixed(2)}</span> |
| <span class="text-xs text-green-600 dark:text-green-400 bg-green-100 dark:bg-green-900 px-2 py-1 rounded-full">${product.gst}% GST</span> |
| </div> |
| <p class="text-xs text-gray-500 dark:text-gray-400 mt-2">Stock: ${product.stock}</p> |
| </div> |
| `; |
| } |
| |
| static createCartItem(item, isPurchase = false) { |
| const updateFunction = isPurchase ? `updatePurchaseQuantity(${item.id}, ${item.quantity - 1})` : `updateQuantity(${item.id}, ${item.quantity - 1})`; |
| const removeFunction = isPurchase ? `removeFromPurchaseCart(${item.id})` : `removeFromCart(${item.id})`; |
| const price = isPurchase ? (item.purchasePrice || item.price) : item.price; |
| |
| return ` |
| <div class="order-item flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded-lg slide-in"> |
| <div class="flex-1"> |
| <h4 class="font-medium text-sm text-gray-800 dark:text-white">${item.name}</h4> |
| <p class="text-xs text-gray-600 dark:text-gray-300">₹${price.toFixed(2)} × ${item.qty}</p> |
| ${isPurchase ? `<p class="text-xs text-blue-600 dark:text-blue-400">Purchase: ₹${(item.purchasePrice || item.price).toFixed(2)}</p>` : ''} |
| </div> |
| <div class="flex items-center space-x-2"> |
| <input type="number" min="0" value="${item.qty}" onchange="${isPurchase ? `updatePurchaseQuantity(${item.id}, this.value)` : `updateQuantity(${item.id}, this.value)`}" class="w-16 px-2 py-1 border border-gray-300 dark:border-gray-600 rounded text-center text-sm bg-white dark:bg-gray-800"> |
| <button onclick="${removeFunction}" class="text-red-600 hover:text-red-700 transition-colors"> |
| <i data-feather="x" class="w-4 h-4"></i> |
| </button> |
| </div> |
| </div> |
| `; |
| } |
| |
| static createCustomerCard(customer, onSelect) { |
| return ` |
| <div class="p-4 hover:bg-blue-50 dark:hover:bg-blue-900/20 cursor-pointer border-b border-gray-100 dark:border-gray-700 last:border-b-0 transition-colors" onclick="${onSelect}"> |
| <div class="flex items-start justify-between"> |
| <div class="flex-1"> |
| <p class="font-semibold text-gray-900 dark:text-white">${customer.name}</p> |
| <p class="text-sm text-gray-600 dark:text-gray-300 mt-1 flex items-center gap-1"> |
| <i data-feather="package" class="w-3 h-3"></i> |
| ${customer.shop} |
| </p> |
| <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">📱 ${customer.mobile}</p> |
| </div> |
| <span class="text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 px-2 py-1 rounded"> |
| ${customer.code} |
| </span> |
| </div> |
| </div> |
| `; |
| } |
| |
| static createOrderRow(order) { |
| return ` |
| <tr class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"> |
| <td class="px-4 py-3 text-sm font-medium text-blue-600 dark:text-blue-400">${order.orderNo}</td> |
| <td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300">${order.date}</td> |
| <td class="px-4 py-3 text-sm font-medium text-gray-800 dark:text-white">${order.customer.name}</td> |
| <td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300">${order.customer.shop}</td> |
| <td class="px-4 py-3 text-sm text-right text-gray-600 dark:text-gray-300">${order.items.reduce((sum, item) => sum + item.qty, 0)}</td> |
| <td class="px-4 py-3 text-sm text-right text-gray-600 dark:text-gray-300">₹${order.subtotal}</td> |
| <td class="px-4 py-3 text-sm text-right text-gray-600 dark:text-gray-300">₹${order.gst}</td> |
| <td class="px-4 py-3 text-sm text-right font-bold text-green-600 dark:text-green-400">₹${order.total}</td> |
| <td class="px-4 py-3 text-center"> |
| <button onclick="printOrderFromReport(${order.id})" class="inline-flex items-center gap-1 bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1.5 rounded-lg transition-colors text-sm"> |
| <i data-feather="printer" class="w-4 h-4"></i> |
| Print |
| </button> |
| </td> |
| </tr> |
| `; |
| } |
| |
| static createProductSummaryRow(product) { |
| return ` |
| <tr class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"> |
| <td class="px-4 py-3 text-sm font-medium text-gray-800 dark:text-white">${product.name}</td> |
| <td class="px-4 py-3 text-sm text-right text-gray-600 dark:text-gray-300">${product.qty}</td> |
| <td class="px-4 py-3 text-sm text-right font-semibold text-green-600 dark:text-green-400">₹${product.amount.toFixed(2)}</td> |
| </tr> |
| `; |
| } |
| } |
| |
| // Navigation Component |
| class Navigation { |
| static tabs = [ |
| { id: 'order', name: 'New Order', icon: 'shopping-cart' }, |
| { id: 'products', name: 'Products', icon: 'package' }, |
| { id: 'customers', name: 'Customers', icon: 'users' }, |
| { id: 'reports', name: 'Reports', icon: 'file-text' } |
| ]; |
| |
| static render() { |
| const navContainer = document.getElementById('nav-tabs'); |
| const state = appState.getState(); |
| |
| navContainer.innerHTML = this.tabs.map(tab => ` |
| <button onclick="Navigation.switchTab('${tab.id}')" |
| class="flex items-center space-x-2 px-4 py-2 rounded-lg font-medium transition-all duration-300 whitespace-nowrap ${state.activeTab === tab.id ? 'tab-active' : 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}"> |
| <i data-feather="${tab.icon}" class="w-4 h-4"></i> |
| <span>${tab.name}</span> |
| ${tab.id === 'products' && state.products.length > 0 ? `<span class="ml-1 text-xs bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400 px-2 py-0.5 rounded-full">${state.products.length}</span>` : ''} |
| ${tab.id === 'customers' && state.customers.length > 0 ? `<span class="ml-1 text-xs bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-400 px-2 py-0.5 rounded-full">${state.customers.length}</span>` : ''} |
| ${tab.id === 'reports' && state.orders.length > 0 ? `<span class="ml-1 text-xs bg-purple-100 dark:bg-purple-900 text-purple-600 dark:text-purple-400 px-2 py-0.5 rounded-full">${state.orders.length}</span>` : ''} |
| </button> |
| `).join(''); |
| |
| feather.replace(); |
| } |
| |
| static switchTab(tabId) { |
| appState.setState({ activeTab: tabId }); |
| } |
| } |
| |
| // Content Renderer |
| class ContentRenderer { |
| static render() { |
| const state = appState.getState(); |
| const contentArea = document.getElementById('content-area'); |
| |
| switch(state.activeTab) { |
| case 'order': |
| this.renderOrderTab(contentArea); |
| break; |
| case 'products': |
| this.renderProductsTab(contentArea); |
| break; |
| case 'customers': |
| this.renderCustomersTab(contentArea); |
| break; |
| case 'reports': |
| this.renderReportsTab(contentArea); |
| break; |
| } |
| |
| feather.replace(); |
| } |
| |
| static renderOrderTab(container) { |
| const state = appState.getState(); |
| const filteredProducts = state.products.filter(p => |
| (p.name && p.name.toLowerCase().includes(state.searchTerm.toLowerCase())) || |
| (p.code && p.code.toLowerCase().includes(state.searchTerm.toLowerCase())) |
| ); |
| |
| const filteredCustomers = state.customers.filter(c => |
| (c.name && c.name.toLowerCase().includes(state.customerSearch.toLowerCase())) || |
| (c.shop && c.shop.toLowerCase().includes(state.customerSearch.toLowerCase())) || |
| (c.mobile && c.mobile.includes(state.customerSearch)) || |
| (c.code && c.code.toLowerCase().includes(state.customerSearch.toLowerCase())) |
| ); |
| |
| const totals = this.calculateTotals(state.currentOrder); |
| |
| container.innerHTML = ` |
| <div class="grid lg:grid-cols-3 gap-6"> |
| |
| <div class="lg:col-span-2 space-y-4"> |
| |
| <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"> |
| <h3 class="text-lg font-bold text-gray-800 dark:text-white mb-4 flex items-center gap-2"> |
| <i data-feather="users" class="w-5 h-5 text-blue-600"></i> |
| Customer Information |
| </h3> |
| |
| ${!state.selectedCustomer ? ` |
| <div class="relative customer-search-container"> |
| <div class="relative"> |
| <i data-feather="search" class="absolute left-3 top-3.5 w-5 h-5 text-gray-400"></i> |
| <input |
| type="text" |
| placeholder="Search customer by name, shop, mobile or code..." |
| value="${state.customerSearch}" |
| oninput="OrderSystem.updateCustomerSearch(this.value)" |
| onfocus="OrderSystem.showCustomerDropdown(true)" |
| class="w-full pl-10 pr-4 py-3 border-2 border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all bg-white dark:bg-gray-700 text-gray-800 dark:text-white" |
| /> |
| </div> |
| |
| ${state.showCustomerDropdown && state.customerSearch && filteredCustomers.length > 0 ? ` |
| <div class="absolute z-10 w-full mt-2 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg shadow-xl customer-dropdown"> |
| ${filteredCustomers.map(customer => UIComponents.createCustomerCard(customer, `OrderSystem.selectCustomer(${customer.id})`)).join('')} |
| </div> |
| ` : ''} |
| |
| ${state.showCustomerDropdown && state.customerSearch && filteredCustomers.length === 0 ? ` |
| <div class="absolute z-10 w-full mt-2 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg shadow-xl p-4"> |
| <p class="text-center text-gray-500 dark:text-gray-400">No customers found</p> |
| </div> |
| ` : ''} |
| </div> |
| ` : ` |
| <div class="relative"> |
| <div class="p-5 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-lg border-2 border-blue-200 dark:border-blue-700"> |
| <button onclick="OrderSystem.selectCustomer(null)" class="absolute top-3 right-3 text-gray-400 hover:text-red-600 transition-colors"> |
| <i data-feather="x" class="w-5 h-5"></i> |
| </button> |
| <div class="flex items-start gap-3"> |
| <div class="bg-blue-600 rounded-full p-2"> |
| <i data-feather="users" class="w-5 h-5 text-white"></i> |
| </div> |
| <div class="flex-1"> |
| <p class="font-bold text-lg text-blue-900 dark:text-blue-100">${state.selectedCustomer.name}</p> |
| <p class="text-sm text-blue-700 dark:text-blue-300 mt-1 flex items-center gap-1"> |
| <i data-feather="package" class="w-4 h-4"></i> |
| ${state.selectedCustomer.shop} |
| </p> |
| <div class="flex gap-4 mt-2 text-sm text-blue-600 dark:text-blue-400"> |
| <span>📱 ${state.selectedCustomer.mobile}</span> |
| ${state.selectedCustomer.code ? `<span>🔖 ${state.selectedCustomer.code}</span>` : ''} |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| `} |
| </div> |
| |
| |
| <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"> |
| <div class="mb-4"> |
| <div class="relative"> |
| <i data-feather="search" class="absolute left-3 top-3 w-5 h-5 text-gray-400"></i> |
| <input |
| type="text" |
| placeholder="Search products..." |
| value="${state.searchTerm}" |
| oninput="OrderSystem.updateSearchTerm(this.value)" |
| class="w-full pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-800 dark:text-white" |
| /> |
| </div> |
| </div> |
| <div class="grid gap-3 max-h-96 overflow-y-auto custom-scrollbar"> |
| ${filteredProducts.map(product => UIComponents.createProductCard(product, `OrderSystem.addToOrder(${product.id})`)).join('')} |
| </div> |
| ${filteredProducts.length === 0 && state.searchTerm ? ` |
| <div class="text-center py-8 text-gray-500 dark:text-gray-400"> |
| <i data-feather="package" class="w-12 h-12 mx-auto mb-2 opacity-50"></i> |
| <p>No products found matching "${state.searchTerm}"</p> |
| </div> |
| ` : ''} |
| </div> |
| </div> |
| |
| |
| <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 lg:sticky lg:top-24 h-fit"> |
| <h3 class="text-lg font-bold text-gray-800 dark:text-white mb-4">Current Order</h3> |
| |
| <div class="space-y-3 mb-4 max-h-64 overflow-y-auto custom-scrollbar"> |
| ${state.currentOrder.map(item => UIComponents.createCartItem(item)).join('')} |
| </div> |
| |
| ${state.currentOrder.length > 0 ? ` |
| <div class="border-t border-gray-200 dark:border-gray-600 pt-4 space-y-2"> |
| <div class="flex justify-between text-sm"> |
| <span class="text-gray-600 dark:text-gray-400">Subtotal:</span> |
| <span class="font-semibold text-gray-800 dark:text-white">₹${totals.subtotal}</span> |
| </div> |
| <div class="flex justify-between text-sm"> |
| <span class="text-gray-600 dark:text-gray-400">GST:</span> |
| <span class="font-semibold text-gray-800 dark:text-white">₹${totals.gst}</span> |
| </div> |
| <div class="flex justify-between text-lg font-bold border-t border-gray-200 dark:border-gray-600 pt-2"> |
| <span class="text-gray-800 dark:text-white">Total:</span> |
| <span class="text-blue-600 dark:text-blue-400">₹${totals.total}</span> |
| </div> |
| </div> |
| |
| <div class="mt-6 space-y-2"> |
| <button onclick="OrderSystem.printOrder()" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white py-3 rounded-lg transition-colors font-semibold flex items-center justify-center gap-2"> |
| <i data-feather="printer" class="w-5 h-5"></i> |
| Print Order |
| </button> |
| <button onclick="OrderSystem.saveOrder()" class="w-full bg-green-600 hover:bg-green-700 text-white py-3 rounded-lg transition-colors font-semibold"> |
| Save Order |
| </button> |
| <button onclick="OrderSystem.exportToExcel()" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-3 rounded-lg transition-colors font-semibold flex items-center justify-center gap-2"> |
| <i data-feather="download" class="w-5 h-5"></i> |
| Export All Orders |
| </button> |
| </div> |
| ` : ` |
| <div class="text-center py-8 text-gray-500 dark:text-gray-400"> |
| <i data-feather="shopping-cart" class="w-12 h-12 mx-auto mb-2 opacity-50"></i> |
| <p>No items in order</p> |
| </div> |
| `} |
| </div> |
| </div> |
| `; |
| } |
| |
| static renderProductsTab(container) { |
| const state = appState.getState(); |
| |
| container.innerHTML = ` |
| <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"> |
| <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 gap-4"> |
| <h2 class="text-xl font-bold text-gray-800 dark:text-white">Product Master</h2> |
| <div class="flex gap-2"> |
| <button onclick="document.getElementById('product-file-input').click()" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors flex items-center gap-2"> |
| <i data-feather="upload" class="w-4 h-4"></i> |
| Upload Excel |
| </button> |
| <button onclick="OrderSystem.downloadProductTemplate()" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors flex items-center gap-2"> |
| <i data-feather="download" class="w-4 h-4"></i> |
| Template |
| </button> |
| </div> |
| </div> |
| <input type="file" id="product-file-input" accept=".xlsx,.xls" class="hidden" onchange="OrderSystem.handleProductUpload(event)"> |
| |
| ${state.products.length > 0 ? ` |
| <div class="overflow-x-auto"> |
| <table class="w-full"> |
| <thead class="bg-gray-50 dark:bg-gray-700"> |
| <tr> |
| <th class="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Code</th> |
| <th class="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Product Name</th> |
| <th class="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Category</th> |
| <th class="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">Price</th> |
| <th class="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">GST %</th> |
| <th class="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">Stock</th> |
| </tr> |
| </thead> |
| <tbody class="divide-y divide-gray-200 dark:divide-gray-600"> |
| ${state.products.map(product => ` |
| <tr class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"> |
| <td class="px-4 py-3 text-sm text-gray-800 dark:text-white">${product.code}</td> |
| <td class="px-4 py-3 text-sm font-medium text-gray-800 dark:text-white">${product.name}</td> |
| <td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300">${product.category}</td> |
| <td class="px-4 py-3 text-sm text-right text-gray-800 dark:text-white">₹${product.price.toFixed(2)}</td> |
| <td class="px-4 py-3 text-sm text-right text-gray-600 dark:text-gray-300">${product.gst}%</td> |
| <td class="px-4 py-3 text-sm text-right ${product.stock < 10 ? 'text-red-600 dark:text-red-400 font-semibold' : 'text-gray-600 dark:text-gray-300'}">${product.stock}</td> |
| </tr> |
| `).join('')} |
| </tbody> |
| </table> |
| </div> |
| ` : ` |
| <div class="text-center py-12 text-gray-500 dark:text-gray-400"> |
| <i data-feather="package" class="w-16 h-16 mx-auto mb-4 opacity-50"></i> |
| <h3 class="text-lg font-semibold mb-2">No Products Found</h3> |
| <p class="mb-4">Upload your product data using Excel file</p> |
| <button onclick="document.getElementById('product-file-input').click()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg transition-colors"> |
| Upload Products |
| </button> |
| </div> |
| `} |
| </div> |
| `; |
| } |
| |
| static renderCustomersTab(container) { |
| const state = appState.getState(); |
| |
| container.innerHTML = ` |
| <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"> |
| <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 gap-4"> |
| <h2 class="text-xl font-bold text-gray-800 dark:text-white">Customer Master</h2> |
| <div class="flex gap-2"> |
| <button onclick="document.getElementById('customer-file-input').click()" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors flex items-center gap-2"> |
| <i data-feather="upload" class="w-4 h-4"></i> |
| Upload Excel |
| </button> |
| <button onclick="OrderSystem.downloadCustomerTemplate()" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors flex items-center gap-2"> |
| <i data-feather="download" class="w-4 h-4"></i> |
| Template |
| </button> |
| </div> |
| </div> |
| <input type="file" id="customer-file-input" accept=".xlsx,.xls" class="hidden" onchange="OrderSystem.handleCustomerUpload(event)"> |
| |
| ${state.customers.length > 0 ? ` |
| <div class="overflow-x-auto"> |
| <table class="w-full"> |
| <thead class="bg-gray-50 dark:bg-gray-700"> |
| <tr> |
| <th class="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Code</th> |
| <th class="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Name</th> |
| <th class="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Mobile</th> |
| <th class="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Address</th> |
| <th class="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">Balance</th> |
| </tr> |
| </thead> |
| <tbody class="divide-y divide-gray-200 dark:divide-gray-600"> |
| ${state.customers.map(customer => ` |
| <tr class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"> |
| <td class="px-4 py-3 text-sm text-gray-800 dark:text-white">${customer.code}</td> |
| <td class="px-4 py-3 text-sm font-medium text-gray-800 dark:text-white">${customer.name}</td> |
| <td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300">${customer.mobile}</td> |
| <td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300">${customer.address}</td> |
| <td class="px-4 py-3 text-sm text-right font-semibold ${customer.balance > 0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}">₹${customer.balance.toFixed(2)}</td> |
| </tr> |
| `).join('')} |
| </tbody> |
| </table> |
| </div> |
| ` : ` |
| <div class="text-center py-12 text-gray-500 dark:text-gray-400"> |
| <i data-feather="users" class="w-16 h-16 mx-auto mb-4 opacity-50"></i> |
| <h3 class="text-lg font-semibold mb-2">No Customers Found</h3> |
| <p class="mb-4">Upload your customer data using Excel file</p> |
| <button onclick="document.getElementById('customer-file-input').click()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg transition-colors"> |
| Upload Customers |
| </button> |
| </div> |
| `} |
| </div> |
| `; |
| } |
| |
| static renderReportsTab(container) { |
| const state = appState.getState(); |
| const filteredOrders = this.getFilteredOrders(state.orders, state.startDate, state.endDate); |
| const summary = this.getReportSummary(filteredOrders); |
| const productSales = this.getProductSalesSummary(filteredOrders); |
| |
| container.innerHTML = ` |
| <div class="space-y-6"> |
| |
| <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"> |
| <h3 class="text-lg font-bold text-gray-800 dark:text-white mb-4 flex items-center gap-2"> |
| <i data-feather="calendar" class="w-5 h-5"></i> |
| Date Filter |
| </h3> |
| <div class="grid md:grid-cols-3 gap-4"> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">From Date</label> |
| <input |
| type="date" |
| value="${state.startDate}" |
| onchange="OrderSystem.updateDateFilter('startDate', this.value)" |
| class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-800 dark:text-white" |
| /> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">To Date</label> |
| <input |
| type="date" |
| value="${state.endDate}" |
| onchange="OrderSystem.updateDateFilter('endDate', this.value)" |
| class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-800 dark:text-white" |
| /> |
| </div> |
| <div class="flex items-end gap-2"> |
| <button onclick="OrderSystem.clearDateFilters()" class="flex-1 bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors"> |
| Clear |
| </button> |
| <button onclick="OrderSystem.exportFilteredOrders()" class="flex-1 bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors flex items-center justify-center gap-2"> |
| <i data-feather="download" class="w-4 h-4"></i> |
| Export |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="grid md:grid-cols-4 gap-4"> |
| <div class="bg-gradient-to-br from-blue-500 to-blue-600 text-white rounded-xl p-6"> |
| <div class="flex items-center justify-between"> |
| <div> |
| <p class="text-blue-100 text-sm">Total Orders</p> |
| <p class="text-2xl font-bold">${summary.totalOrders}</p> |
| </div> |
| <i data-feather="file-text" class="w-8 h-8 text-blue-200"></i> |
| </div> |
| </div> |
| <div class="bg-gradient-to-br from-green-500 to-green-600 text-white rounded-xl p-6"> |
| <div class="flex items-center justify-between"> |
| <div> |
| <p class="text-green-100 text-sm">Total Amount</p> |
| <p class="text-2xl font-bold">₹${summary.totalAmount}</p> |
| </div> |
| <i data-feather="dollar-sign" class="w-8 h-8 text-green-200"></i> |
| </div> |
| </div> |
| <div class="bg-gradient-to-br from-purple-500 to-purple-600 text-white rounded-xl p-6"> |
| <div class="flex items-center justify-between"> |
| <div> |
| <p class="text-purple-100 text-sm">Total GST</p> |
| <p class="text-2xl font-bold">₹${summary.totalGST}</p> |
| </div> |
| <i data-feather="percent" class="w-8 h-8 text-purple-200"></i> |
| </div> |
| </div> |
| <div class="bg-gradient-to-br from-orange-500 to-orange-600 text-white rounded-xl p-6"> |
| <div class="flex items-center justify-between"> |
| <div> |
| <p class="text-orange-100 text-sm">Total Items</p> |
| <p class="text-2xl font-bold">${summary.totalItems}</p> |
| </div> |
| <i data-feather="package" class="w-8 h-8 text-orange-200"></i> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"> |
| <h3 class="text-lg font-bold text-gray-800 dark:text-white mb-4">Order Details</h3> |
| <div class="overflow-x-auto"> |
| <table class="w-full"> |
| <thead class="bg-gray-50 dark:bg-gray-700"> |
| <tr> |
| <th class="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Order No</th> |
| <th class="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Date</th> |
| <th class="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Customer</th> |
| <th class="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Shop</th> |
| <th class="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">Items</th> |
| <th class="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">Subtotal</th> |
| <th class="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">GST</th> |
| <th class="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">Total</th> |
| <th class="px-4 py-3 text-center text-sm font-semibold text-gray-700 dark:text-gray-300">Action</th> |
| </tr> |
| </thead> |
| <tbody class="divide-y divide-gray-200 dark:divide-gray-600"> |
| ${filteredOrders.length === 0 ? ` |
| <tr> |
| <td colspan="9" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400"> |
| No orders found for selected date range |
| </td> |
| </tr> |
| ` : filteredOrders.map(order => UIComponents.createOrderRow(order)).join('')} |
| </tbody> |
| </table> |
| </div> |
| </div> |
| |
| |
| ${filteredOrders.length > 0 ? ` |
| <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"> |
| <h3 class="text-lg font-bold text-gray-800 dark:text-white mb-4">Product-wise Sales Summary</h3> |
| <div class="overflow-x-auto"> |
| <table class="w-full"> |
| <thead class="bg-gray-50 dark:bg-gray-700"> |
| <tr> |
| <th class="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">Product</th> |
| <th class="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">Quantity Sold</th> |
| <th class="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">Total Amount</th> |
| </tr> |
| </thead> |
| <tbody class="divide-y divide-gray-200 dark:divide-gray-600"> |
| ${productSales.map(product => UIComponents.createProductSummaryRow(product)).join('')} |
| </tbody> |
| </table> |
| </div> |
| </div> |
| ` : ''} |
| </div> |
| `; |
| } |
| |
| static calculateTotals(orderItems) { |
| let subtotal = 0; |
| let totalGst = 0; |
| |
| orderItems.forEach(item => { |
| const itemTotal = item.price * item.qty; |
| subtotal += itemTotal; |
| totalGst += (itemTotal * item.gst) / 100; |
| }); |
| |
| return { |
| subtotal: subtotal.toFixed(2), |
| gst: totalGst.toFixed(2), |
| total: (subtotal + totalGst).toFixed(2) |
| }; |
| } |
| |
| static getFilteredOrders(orders, startDate, endDate) { |
| if (!startDate && !endDate) return orders; |
| |
| return orders.filter(order => { |
| const orderDate = new Date(order.dateObj); |
| const start = startDate ? new Date(startDate) : null; |
| const end = endDate ? new Date(endDate) : null; |
| |
| if (start && end) { |
| end.setHours(23, 59, 59, 999); |
| return orderDate >= start && orderDate <= end; |
| } else if (start) { |
| return orderDate >= start; |
| } else if (end) { |
| end.setHours(23, 59, 59, 999); |
| return orderDate <= end; |
| } |
| return true; |
| }); |
| } |
| |
| static getReportSummary(orders) { |
| const totalOrders = orders.length; |
| const totalAmount = orders.reduce((sum, order) => sum + parseFloat(order.total), 0); |
| const totalGST = orders.reduce((sum, order) => sum + parseFloat(order.gst), 0); |
| const totalItems = orders.reduce((sum, order) => |
| sum + order.items.reduce((itemSum, item) => itemSum + item.qty, 0), 0 |
| ); |
| |
| return { |
| totalOrders, |
| totalAmount: totalAmount.toFixed(2), |
| totalGST: totalGST.toFixed(2), |
| totalItems |
| }; |
| } |
| |
| static getProductSalesSummary(orders) { |
| const productSales = {}; |
| orders.forEach(order => { |
| order.items.forEach(item => { |
| if (!productSales[item.id]) { |
| productSales[item.id] = { |
| name: item.name, |
| qty: 0, |
| amount: 0 |
| }; |
| } |
| productSales[item.id].qty += item.qty; |
| productSales[item.id].amount += item.price * item.qty; |
| }); |
| }); |
| |
| return Object.values(productSales) |
| .sort((a, b) => b.amount - a.amount); |
| } |
| } |
| |
| // Order System |
| class OrderSystem { |
| static updateSearchTerm(term) { |
| appState.setState({ searchTerm: term }); |
| } |
| |
| static updateCustomerSearch(term) { |
| appState.setState({ |
| customerSearch: term, |
| showCustomerDropdown: term.length > 0 |
| }); |
| } |
| |
| static showCustomerDropdown(show) { |
| appState.setState({ showCustomerDropdown: show }); |
| } |
| |
| static selectCustomer(customerId) { |
| const state = appState.getState(); |
| const customer = state.customers.find(c => c.id === customerId); |
| appState.setState({ |
| selectedCustomer: customer, |
| customerSearch: '', |
| showCustomerDropdown: false |
| }); |
| } |
| |
| static addToOrder(productId) { |
| const state = appState.getState(); |
| const product = state.products.find(p => p.id === productId); |
| const existingItem = state.currentOrder.find(item => item.id === productId); |
| |
| if (existingItem) { |
| const updatedOrder = state.currentOrder.map(item => |
| item.id === productId ? { ...item, qty: item.qty + 1 } : item |
| ); |
| appState.setState({ currentOrder: updatedOrder }); |
| } else { |
| appState.setState({ |
| currentOrder: [...state.currentOrder, { ...product, qty: 1 }] |
| }); |
| } |
| } |
| |
| static updateQuantity(productId, newQuantity) { |
| const state = appState.getState(); |
| const quantity = parseInt(newQuantity); |
| |
| if (quantity <= 0) { |
| const updatedOrder = state.currentOrder.filter(item => item.id !== productId); |
| appState.setState({ currentOrder: updatedOrder }); |
| } else { |
| const updatedOrder = state.currentOrder.map(item => |
| item.id === productId ? { ...item, qty: quantity } : item |
| ); |
| appState.setState({ currentOrder: updatedOrder }); |
| } |
| } |
| |
| static calculateTotals(orderItems) { |
| let subtotal = 0; |
| let totalGst = 0; |
| |
| orderItems.forEach(item => { |
| const itemTotal = item.price * item.qty; |
| subtotal += itemTotal; |
| totalGst += (itemTotal * item.gst) / 100; |
| }); |
| |
| return { |
| subtotal: subtotal.toFixed(2), |
| gst: totalGst.toFixed(2), |
| total: (subtotal + totalGst).toFixed(2) |
| }; |
| } |
| |
| static saveOrder() { |
| const state = appState.getState(); |
| |
| if (!state.selectedCustomer || state.currentOrder.length === 0) { |
| this.showNotification('Please select customer and add items to order', 'error'); |
| return; |
| } |
| |
| const totals = this.calculateTotals(state.currentOrder); |
| const newOrder = { |
| id: state.orders.length + 1, |
| orderNo: `ORD${String(state.orders.length + 1).padStart(5, '0')}`, |
| date: new Date().toLocaleString('en-IN'), |
| dateObj: new Date(), |
| customer: state.selectedCustomer, |
| items: [...state.currentOrder], |
| ...totals |
| }; |
| |
| appState.setState({ |
| orders: [...state.orders, newOrder], |
| currentOrder: [], |
| selectedCustomer: null |
| }); |
| |
| this.showNotification('Order saved successfully!', 'success'); |
| } |
| |
| static printOrder() { |
| const state = appState.getState(); |
| |
| if (!state.selectedCustomer || state.currentOrder.length === 0) { |
| this.showNotification('Please select customer and add items to order', 'error'); |
| return; |
| } |
| |
| const totals = this.calculateTotals(state.currentOrder); |
| const orderNo = `ORD${String(state.orders.length + 1).padStart(5, '0')}`; |
| |
| const printContent = ` |
| <html> |
| <head> |
| <title>Order Slip - ${orderNo}</title> |
| <style> |
| @media print { |
| @page { size: 80mm auto; margin: 0; } |
| body { margin: 0; } |
| } |
| body { |
| width: 80mm; |
| font-family: 'Courier New', monospace; |
| font-size: 11px; |
| margin: 5mm; |
| } |
| .center { text-align: center; } |
| .bold { font-weight: bold; } |
| .line { border-top: 1px dashed #000; margin: 5px 0; } |
| table { width: 100%; border-collapse: collapse; } |
| td { padding: 2px 0; } |
| .right { text-align: right; } |
| </style> |
| </head> |
| <body> |
| <div class="center bold" style="font-size: 14px;">${state.shopProfile.shopName}</div> |
| <div class="center" style="font-size: 10px;">${state.shopProfile.address}</div> |
| <div class="center" style="font-size: 10px;">Ph: ${state.shopProfile.phone}</div> |
| <div class="line"></div> |
| |
| <div class="bold">Order No: ${orderNo}</div> |
| <div>Date: ${new Date().toLocaleString('en-IN')}</div> |
| <div class="line"></div> |
| |
| <div class="bold">Customer Details:</div> |
| <div>${state.selectedCustomer.name}</div> |
| <div>${state.selectedCustomer.shop}</div> |
| <div>Ph: ${state.selectedCustomer.mobile}</div> |
| <div class="line"></div> |
| |
| <table> |
| <tr class="bold"> |
| <td>Item</td> |
| <td class="right">Qty</td> |
| <td class="right">Rate</td> |
| <td class="right">Amt</td> |
| </tr> |
| <tr><td colspan="4"><div class="line"></div></td></tr> |
| ${state.currentOrder.map(item => ` |
| <tr> |
| <td>${item.name}</td> |
| <td class="right">${item.qty}</td> |
| <td class="right">${item.price.toFixed(2)}</td> |
| <td class="right">${(item.price * item.qty).toFixed(2)}</td> |
| </tr> |
| `).join('')} |
| </table> |
| |
| <div class="line"></div> |
| <table> |
| <tr> |
| <td>Subtotal:</td> |
| <td class="right">₹${totals.subtotal}</td> |
| </tr> |
| <tr> |
| <td>GST:</td> |
| <td class="right">₹${totals.gst}</td> |
| </tr> |
| <tr class="bold"> |
| <td>Total:</td> |
| <td class="right">₹${totals.total}</td> |
| </tr> |
| </table> |
| <div class="line"></div> |
| |
| <div class="center" style="margin-top: 10px;">Thank You!</div> |
| <div class="center" style="font-size: 9px;">Visit Again</div> |
| |
| <script> |
| window.onload = function() { |
| window.print(); |
| setTimeout(function() { |
| window.close(); |
| }, 100); |
| }; |
| </script> |
| </body> |
| </html> |
| `; |
| |
| const printWindow = window.open('', '_blank', 'width=300,height=600'); |
| if (printWindow) { |
| printWindow.document.open(); |
| printWindow.document.write(printContent); |
| printWindow.document.close(); |
| } else { |
| this.showNotification('Please allow pop-ups to print orders', 'error'); |
| } |
| } |
|
|
| static printOrderFromReport(orderId) { |
| const state = appState.getState(); |
| const order = state.orders.find(o => o.id === orderId); |
| |
| if (!order) return; |
|
|
| const printContent = ` |
| <html> |
| <head> |
| <title>Order Slip - ${order.orderNo}</title> |
| <style> |
| @media print { |
| @page { size: 80mm auto; margin: 0; } |
| body { margin: 0; } |
| } |
| body { |
| width: 80mm; |
| font-family: 'Courier New', monospace; |
| font-size: 11px; |
| margin: 5mm; |
| } |
| .center { text-align: center; } |
| .bold { font-weight: bold; } |
| .line { border-top: 1px dashed #000; margin: 5px 0; } |
| table { width: 100%; border-collapse: collapse; } |
| td { padding: 2px 0; } |
| .right { text-align: right; } |
| </style> |
| </head> |
| <body> |
| <div class="center bold" style="font-size: 14px;">${state.shopProfile.shopName}</div> |
| <div class="center" style="font-size: 10px;">${state.shopProfile.address}</div> |
| <div class="center" style="font-size: 10px;">Ph: ${state.shopProfile.phone}</div> |
| <div class="line"></div> |
| |
| <div class="bold">Order No: ${order.orderNo}</div> |
| <div>Date: ${order.date}</div> |
| <div class="line"></div> |
| |
| <div class="bold">Customer Details:</div> |
| <div>${order.customer.name}</div> |
| <div>${order.customer.shop}</div> |
| <div>Ph: ${order.customer.mobile}</div> |
| <div class="line"></div> |
| |
| <table> |
| <tr class="bold"> |
| <td>Item</td> |
| <td class="right">Qty</td> |
| <td class="right">Rate</td> |
| <td class="right">Amt</td> |
| </tr> |
| <tr><td colspan="4"><div class="line"></div></td></tr> |
| ${order.items.map(item => ` |
| <tr> |
| <td>${item.name}</td> |
| <td class="right">${item.qty}</td> |
| <td class="right">${item.price.toFixed(2)}</td> |
| <td class="right">${(item.price * item.qty).toFixed(2)}</td> |
| </tr> |
| `).join('')} |
| </table> |
| |
| <div class="line"></div> |
| <table> |
| <tr> |
| <td>Subtotal:</td> |
| <td class="right">₹${order.subtotal}</td> |
| </tr> |
| <tr> |
| <td>GST:</td> |
| <td class="right">₹${order.gst}</td> |
| </tr> |
| <tr class="bold"> |
| <td>Total:</td> |
| <td class="right">₹${order.total}</td> |
| </tr> |
| </table> |
| <div class="line"></div> |
| |
| <div class="center" style="margin-top: 10px;">Thank You!</div> |
| <div class="center" style="font-size: 9px;">Visit Again</div> |
| |
| <script> |
| window.onload = function() { |
| window.print(); |
| setTimeout(function() { |
| window.close(); |
| }, 100); |
| }; |
| </script> |
| </body> |
| </html> |
| `; |
| |
| const printWindow = window.open('', '_blank', 'width=300,height=600'); |
| if (printWindow) { |
| printWindow.document.open(); |
| printWindow.document.write(printContent); |
| printWindow.document.close(); |
| } else { |
| this.showNotification('Please allow pop-ups to print orders', 'error'); |
| } |
| } |
|
|
| static handleProductUpload(event) { |
| const file = event.target.files[0]; |
| if (!file) return; |
|
|
| const reader = new FileReader(); |
| reader.onload = (e) => { |
| try { |
| const data = new Uint8Array(e.target.result); |
| const workbook = XLSX.read(data, { type: 'array' }); |
| const worksheet = workbook.Sheets[workbook.SheetNames[0]]; |
| const jsonData = XLSX.utils.sheet_to_json(worksheet); |
| |
| const formattedProducts = jsonData.map((row, idx) => ({ |
| id: idx + 1, |
| code: String(row.Code || row.code || ''), |
| name: String(row.Name || row.name || row.Product || ''), |
| category: String(row.Category || row.category || ''), |
| price: parseFloat(row.Price || row.price || 0), |
| mrp: parseFloat(row.MRP || row.mrp || 0), |
| gst: parseFloat(row.GST || row.gst || 0), |
| stock: parseInt(row.Stock || row.stock || 0) |
| })); |
| |
| appState.setState({ products: formattedProducts }); |
| this.showNotification('Products uploaded successfully!', 'success'); |
| } catch (error) { |
| this.showNotification('Error uploading products. Please check file format.', 'error'); |
| } |
| }; |
| reader.readAsArrayBuffer(file); |
| } |
|
|
| static handleCustomerUpload(event) { |
| const file = event.target.files[0]; |
| if (!file) return; |
|
|
| const reader = new FileReader(); |
| reader.onload = (e) => { |
| try { |
| const data = new Uint8Array(e.target.result); |
| const workbook = XLSX.read(data, { type: 'array' }); |
| const worksheet = workbook.Sheets[workbook.SheetNames[0]]; |
| const jsonData = XLSX.utils.sheet_to_json(worksheet); |
| |
| const formattedCustomers = jsonData.map((row, idx) => ({ |
| id: idx + 1, |
| code: String(row.Code || row.code || ''), |
| name: String(row.Name || row.name || ''), |
| shop: String(row.Shop || row.shop || row['Shop Name'] || ''), |
| mobile: String(row.Mobile || row.mobile || row.Phone || ''), |
| address: String(row.Address || row.address || ''), |
| gstin: String(row.GSTIN || row.gstin || ''), |
| balance: parseFloat(row.Balance || row.balance || 0) |
| })); |
| |
| appState.setState({ customers: formattedCustomers }); |
| this.showNotification('Customers uploaded successfully!', 'success'); |
| } catch (error) { |
| this.showNotification('Error uploading customers. Please check file format.', 'error'); |
| } |
| }; |
| reader.readAsArrayBuffer(file); |
| } |
|
|
| static exportToExcel() { |
| const state = appState.getState(); |
| if (state.orders.length === 0) { |
| this.showNotification('No orders to export', 'error'); |
| return; |
| } |
|
|
| const exportData = []; |
| state.orders.forEach(order => { |
| order.items.forEach(item => { |
| exportData.push({ |
| 'Order No': order.orderNo, |
| 'Date': order.date, |
| 'Customer Name': order.customer.name, |
| 'Customer Shop': order.customer.shop, |
| 'Customer Mobile': order.customer.mobile, |
| 'Product Code': item.code, |
| 'Product Name': item.name, |
| 'Category': item.category, |
| 'Quantity': item.qty, |
| 'Rate': item.price, |
| 'Amount': (item.price * item.qty).toFixed(2), |
| 'GST %': item.gst, |
| 'Order Subtotal': order.subtotal, |
| 'Order GST': order.gst, |
| 'Order Total': order.total |
| }); |
| }); |
| }); |
|
|
| const ws = XLSX.utils.json_to_sheet(exportData); |
| const wb = XLSX.utils.book_new(); |
| XLSX.utils.book_append_sheet(wb, ws, 'Orders'); |
| XLSX.writeFile(wb, `Orders_${new Date().toISOString().split('T')[0]}.xlsx`); |
| } |
|
|
| static exportFilteredOrders() { |
| const state = appState.getState(); |
| const filteredOrders = ContentRenderer.getFilteredOrders(state.orders, state.startDate, state.endDate); |
| |
| if (filteredOrders.length === 0) { |
| this.showNotification('No orders to export for selected date range', 'error'); |
| return; |
| } |
|
|
| const exportData = []; |
| filteredOrders.forEach(order => { |
| order.items.forEach(item => { |
| exportData.push({ |
| 'Order No': order.orderNo, |
| 'Date': order.date, |
| 'Customer Name': order.customer.name, |
| 'Customer Shop': order.customer.shop, |
| 'Customer Mobile': order.customer.mobile, |
| 'Product Code': item.code, |
| 'Product Name': item.name, |
| 'Category': item.category, |
| 'Quantity': item.qty, |
| 'Rate': item.price, |
| 'Amount': (item.price * item.qty).toFixed(2), |
| 'GST %': item.gst, |
| 'Order Subtotal': order.subtotal, |
| 'Order GST': order.gst, |
| 'Order Total': order.total |
| }); |
| }); |
| }); |
|
|
| const ws = XLSX.utils.json_to_sheet(exportData); |
| const wb = XLSX.utils.book_new(); |
| XLSX.utils.book_append_sheet(wb, ws, 'Orders'); |
| const filename = state.startDate && state.endDate |
| ? `Orders_${state.startDate}_to_${state.endDate}.xlsx` |
| : `Orders_${new Date().toISOString().split('T')[0]}.xlsx`; |
| XLSX.writeFile(wb, filename); |
| } |
|
|
| static updateDateFilter(type, value) { |
| const state = appState.getState(); |
| if (type === 'startDate') { |
| appState.setState({ startDate: value }); |
| } else { |
| appState.setState({ endDate: value }); |
| } |
| } |
|
|
| static clearDateFilters() { |
| appState.setState({ startDate: '', endDate: '' }); |
| } |
|
|
| static downloadProductTemplate() { |
| const template = [ |
| { Code: 'P001', Name: 'Sample Product', Category: 'Category A', Price: 100, MRP: 120, GST: 18, Stock: 50 }, |
| { Code: 'P002', Name: 'Another Product', Category: 'Category B', Price: 200, MRP: 250, GST: 12, Stock: 30 } |
| ]; |
| |
| const ws = XLSX.utils.json_to_sheet(template); |
| const wb = XLSX.utils.book_new(); |
| XLSX.utils.book_append_sheet(wb, ws, 'Products'); |
| XLSX.writeFile(wb, 'Product_Template.xlsx'); |
| } |
|
|
| static downloadCustomerTemplate() { |
| const template = [ |
| { Code: 'C001', Name: 'John Doe', Shop: 'ABC Store', Mobile: '9876543210', Address: '123 Main St', GSTIN: '27AABCCDDEEFFG', Balance: 0 }, |
| { Code: 'C002', Name: 'Jane Smith', Shop: 'XYZ Mart', Mobile: '9123456789', Address: '456 Market Rd', GSTIN: '27XYZPQRST1234Z', Balance: 500 } |
| ]; |
| |
| const ws = XLSX.utils.json_to_sheet(template); |
| const wb = XLSX.utils.book_new(); |
| XLSX.utils.book_append_sheet(wb, ws, 'Customers'); |
| XLSX.writeFile(wb, 'Customer_Template.xlsx'); |
| } |
|
|
| static showNotification(message, type) { |
| const notification = document.createElement('div'); |
| notification.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg transition-all duration-300 ${ |
| type === 'success' ? 'bg-green-500 text-white' : 'bg-red-500 text-white' |
| }`; |
| notification.innerHTML = ` |
| <div class="flex items-center gap-2"> |
| <i data-feather="${type === 'success' ? 'check-circle' : 'alert-circle'}" class="w-5 h-5"></i> |
| <span>${message}</span> |
| </div> |
| `; |
| |
| document.body.appendChild(notification); |
| feather.replace(); |
| |
| setTimeout(() => { |
| notification.style.transform = 'translateX(100%)'; |
| setTimeout(() => notification.remove(), 300); |
| }, 3000); |
| } |
| } |
|
|
| // Theme Manager |
| class ThemeManager { |
| static init() { |
| const savedTheme = localStorage.getItem('theme') || 'light'; |
| this.setTheme(savedTheme); |
| |
| document.getElementById('theme-toggle').addEventListener('click', () => { |
| const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light'; |
| const newTheme = currentTheme === 'light' ? 'dark' : 'light'; |
| this.setTheme(newTheme); |
| }); |
| } |
|
|
| static setTheme(theme) { |
| if (theme === 'dark') { |
| document.documentElement.classList.add('dark'); |
| } else { |
| document.documentElement.classList.remove('dark'); |
| } |
| localStorage.setItem('theme', theme); |
| appState.setState({ theme }); |
| } |
| } |
|
|
| // Initialize App |
| function initializeApp() { |
| // Subscribe to state changes |
| appState.subscribe((state) => { |
| Navigation.render(); |
| ContentRenderer.render(); |
| }); |
|
|
| // Initialize theme |
| ThemeManager.init(); |
|
|
| // Hide loading screen and show main app |
| setTimeout(() => { |
| document.getElementById('loading-screen').style.display = 'none'; |
| document.getElementById('main-app').style.display = 'block'; |
| |
| // Initial render |
| Navigation.render(); |
| ContentRenderer.render(); |
| |
| // Add some sample data for demo |
| if (appState.getState().products.length === 0) { |
| appState.setState({ |
| products: [ |
| { id: 1, code: 'P001', name: 'Parle-G Biscuits', category: 'Snacks', price: 10, mrp: 12, gst: 18, stock: 100 }, |
| { id: 2, code: 'P002', name: 'Dairy Milk Chocolate', category: 'Chocolates', price: 20, mrp: 25, gst: 12, stock: 50 }, |
| { id: 3, code: 'P003', name: 'Nescafe Coffee', category: 'Beverages', price: 180, mrp: 200, gst: 18, stock: 30 }, |
| { id: 4, code: 'P004', name: 'Amul Butter', category: 'Dairy', price: 45, mrp: 50, gst: 5, stock: 40 }, |
| { id: 5, code: 'P005', name: 'Colgate Toothpaste', category: 'Personal Care', price: 85, mrp: 95, gst: 18, stock: 60 } |
| ], |
| customers: [ |
| { id: 1, code: 'C001', name: 'Rajesh Kumar', shop: 'Shree Ganesh Traders', mobile: '9876543210', address: '123 Main Street, Mumbai', gstin: '27AABCCDDEEFFG', balance: 0 }, |
| { id: 2, code: 'C002', name: 'Suresh Patel', shop: 'Patel General Store', mobile: '9123456789', address: '456 Market Road, Delhi', gstin: '07XYZPQRST1234Z', balance: 500 } |
| ] |
| }); |
| } |
| }, 2000); |
| } |
|
|
| // Close dropdown when clicking outside |
| document.addEventListener('click', (event) => { |
| const state = appState.getState(); |
| if (state.showCustomerDropdown && !event.target.closest('.customer-search-container')) { |
| appState.setState({ showCustomerDropdown: false }); |
| } |
| }); |
|
|
| // Initialize the app when DOM is loaded |
| document.addEventListener('DOMContentLoaded', initializeApp); |
| </script> |
| </body> |
| </html> |