Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Admin Dashboard - Loan & Device Management</title> | |
| <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); | |
| :root { | |
| --primary: #4f46e5; --secondary: #db2777; --success: #10b981; | |
| --warning: #f59e0b; --danger: #ef4444; --info: #3b82f6; | |
| } | |
| body { font-family: 'Inter', sans-serif; background-color: #f1f5f9; } | |
| .card { background: white; border-radius: 16px; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); } | |
| .status-badge { padding: 4px 12px; border-radius: 20px; font-size: 11px; font-weight: 600; text-transform: uppercase; } | |
| .active-badge { background-color: #ecfdf5; color: #065f46; } | |
| .blocked-badge, .on-hold-badge { background-color: #fefce8; color: #854d0e; } | |
| .paid-off-badge { background-color: #dcfce7; color: #166534; } | |
| .partial-badge { background-color: #fefce8; color: #854d0e; } | |
| .unpaid-badge { background-color: #fee2e2; color: #991b1b; } | |
| .form-input { border: 1px solid #e2e8f0; border-radius: 10px; padding: 10px 14px; width: 100%; background-color: #f8fafc; } | |
| .form-input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15); outline: none; } | |
| .toast { position: fixed; bottom: 30px; right: 30px; padding: 16px 24px; border-radius: 12px; color: white; font-weight: 500; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); z-index: 1050; transform: translateY(120px); opacity: 0; transition: all 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55); } | |
| .toast.show { transform: translateY(0); opacity: 1; } | |
| .btn { border-radius: 10px; padding: 12px 24px; font-weight: 600; transition: all 0.3s; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; } | |
| .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(15, 23, 42, 0.6); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 1000; opacity: 0; pointer-events: none; transition: opacity 0.3s; } | |
| .modal-overlay.active { opacity: 1; pointer-events: all; } | |
| .modal-content { background: white; border-radius: 16px; width: 95%; max-width: 600px; max-height: 90vh; overflow-y: auto; transform: scale(0.95); opacity: 0; transition: all 0.3s ease-out; } | |
| .modal-overlay.active .modal-content { transform: scale(1); opacity: 1; } | |
| .tab-button { padding: 10px 20px; border-radius: 8px; font-weight: 600; } | |
| .tab-button.active { color: var(--primary); background-color: #eef2ff; } | |
| </style> | |
| </head> | |
| <body class="p-4 md:p-8"> | |
| <div id="toast-container" class="fixed bottom-0 right-0 p-8 space-y-3 z-50"></div> | |
| <div id="modal-container"></div> | |
| <div class="max-w-7xl mx-auto space-y-8"> | |
| <!-- HEADER --> | |
| <div class="flex flex-col md:flex-row justify-between items-center"> | |
| <div> | |
| <h1 class="text-3xl font-bold text-gray-800">Admin Dashboard</h1> | |
| <p class="text-gray-600 mt-1">Overview of your loan and device portfolio.</p> | |
| </div> | |
| <button id="open-add-modal-btn" class="btn bg-primary text-white mt-4 md:mt-0"> | |
| <i class="fas fa-plus-circle mr-2"></i> Add New Entry | |
| </button> | |
| </div> | |
| <!-- DASHBOARD STATS --> | |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> | |
| <!-- Financial KPIs --> | |
| <div class="card p-5 flex items-center space-x-4 col-span-1 md:col-span-2 lg:col-span-1"> | |
| <div class="bg-indigo-100 p-4 rounded-full"><i class="fas fa-landmark fa-2x text-indigo-600"></i></div> | |
| <div> | |
| <p class="text-gray-500">Total Loan Value</p> | |
| <p class="text-2xl font-bold text-gray-800" id="stat-total-loan">Rs.0</p> | |
| </div> | |
| </div> | |
| <div class="card p-5 flex items-center space-x-4"> | |
| <div class="bg-green-100 p-4 rounded-full"><i class="fas fa-check-double fa-2x text-green-600"></i></div> | |
| <div> | |
| <p class="text-gray-500">Total Collected</p> | |
| <p class="text-2xl font-bold text-gray-800" id="stat-total-collected">Rs.0</p> | |
| </div> | |
| </div> | |
| <div class="card p-5 flex items-center space-x-4"> | |
| <div class="bg-red-100 p-4 rounded-full"><i class="fas fa-file-invoice-dollar fa-2x text-red-600"></i></div> | |
| <div> | |
| <p class="text-gray-500">Total Outstanding</p> | |
| <p class="text-2xl font-bold text-gray-800" id="stat-total-outstanding">Rs.0</p> | |
| </div> | |
| </div> | |
| <!-- Chart --> | |
| <div class="card p-5 lg:col-span-1 md:col-span-2 col-span-1"> | |
| <h3 class="font-bold text-gray-800 mb-2">Portfolio Status</h3> | |
| <canvas id="status-chart"></canvas> | |
| </div> | |
| </div> | |
| <!-- DATA TABLE --> | |
| <div class="card p-4 sm:p-6"> | |
| <div class="flex flex-col md:flex-row justify-between items-center mb-4 border-b pb-4 gap-4"> | |
| <div class="flex-wrap flex space-x-2"> | |
| <button class="tab-button active" data-tab="all">All (<span id="count-all">0</span>)</button> | |
| <button class="tab-button" data-tab="device">Devices (<span id="count-device">0</span>)</button> | |
| <button class="tab-button" data-tab="loan">Loans (<span id="count-loan">0</span>)</button> | |
| </div> | |
| <div class="flex space-x-3 w-full md:w-auto"> | |
| <div class="relative flex-grow"> | |
| <input type="text" id="search-input" class="form-input pl-10 w-full" placeholder="Search..."> | |
| <i class="fas fa-search absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i> | |
| </div> | |
| <button id="export-btn" class="btn bg-gray-700 hover:bg-gray-800 text-white" title="Export as CSV"><i class="fas fa-file-csv"></i></button> | |
| </div> | |
| </div> | |
| <div class="overflow-x-auto"> | |
| <table class="w-full text-sm"> | |
| <thead> | |
| <tr class="text-left text-gray-500 font-medium border-b-2 border-gray-200"> | |
| <th class="p-3">User / Details</th> | |
| <th class="p-3">Loan Progress</th> | |
| <th class="p-3">Status</th> | |
| <th class="p-3 text-right">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody id="entries-table" class="divide-y divide-gray-100"></tbody> | |
| </table> | |
| <div id="empty-state" class="py-16 text-center hidden"> | |
| <i class="fas fa-inbox text-5xl text-gray-300 mb-4"></i> | |
| <h3 class="text-lg font-medium text-gray-700">No Entries Found</h3> | |
| <p class="text-gray-500 mt-1">Click "Add New Entry" to get started.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // --- STATE & CONFIG --- | |
| let entries = JSON.parse(localStorage.getItem('loanAndDeviceApp_v3')) || []; | |
| let activeTab = 'all'; | |
| let statusChart = null; | |
| // --- DOM ELEMENTS --- | |
| const modalContainer = document.getElementById('modal-container'); | |
| const entriesTable = document.getElementById('entries-table'); | |
| const emptyState = document.getElementById('empty-state'); | |
| const searchInput = document.getElementById('search-input'); | |
| // Add more DOM element references here as needed | |
| // --- TEMPLATES (kept concise for brevity, full logic in functions) --- | |
| const modalTemplate = (type, entry = {}) => { /* ... see previous version, unchanged ... */ }; | |
| // --- HELPER FUNCTIONS --- | |
| const saveData = () => localStorage.setItem('loanAndDeviceApp_v3', JSON.stringify(entries)); | |
| const formatCurrency = (amount) => `Rs.${(amount || 0).toLocaleString('en-IN')}`; | |
| const getFileAsBase64 = (file) => new Promise((resolve) => { | |
| if (!file) { resolve(null); return; } | |
| const reader = new FileReader(); | |
| reader.readAsDataURL(file); | |
| reader.onload = () => resolve({ name: file.name, data: reader.result }); | |
| }); | |
| const showToast = (message, type = 'info') => { | |
| const container = document.getElementById('toast-container'); | |
| if(!container) return; | |
| const toast = document.createElement('div'); | |
| const icons = { success: 'fa-check-circle', error: 'fa-times-circle', info: 'fa-info-circle', warning: 'fa-exclamation-triangle' }; | |
| toast.className = `toast ${type} flex items-center space-x-3`; | |
| toast.innerHTML = `<i class="fas ${icons[type]}"></i><span>${message}</span>`; | |
| container.prepend(toast); | |
| setTimeout(() => toast.classList.add('show'), 10); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| toast.addEventListener('transitionend', () => toast.remove()); | |
| }, 3000); | |
| }; | |
| // --- DASHBOARD & UI RENDERING --- | |
| const updateUI = () => { | |
| renderDashboardStats(); | |
| renderDashboardChart(); | |
| renderTable(); | |
| updateTabCounts(); | |
| }; | |
| const renderDashboardStats = () => { | |
| let totalLoan = 0, totalCollected = 0; | |
| entries.forEach(e => { | |
| totalLoan += e.totalAmount; | |
| totalCollected += e.payments.reduce((sum, p) => sum + p.amount, 0); | |
| }); | |
| document.getElementById('stat-total-loan').textContent = formatCurrency(totalLoan); | |
| document.getElementById('stat-total-collected').textContent = formatCurrency(totalCollected); | |
| document.getElementById('stat-total-outstanding').textContent = formatCurrency(totalLoan - totalCollected); | |
| }; | |
| const renderDashboardChart = () => { | |
| const ctx = document.getElementById('status-chart').getContext('2d'); | |
| let statuses = { 'Active - In Progress': 0, 'Paid Off': 0, 'On Hold / Blocked': 0, 'Active - Unpaid': 0 }; | |
| entries.forEach(e => { | |
| const paidAmount = e.payments.reduce((sum, p) => sum + p.amount, 0); | |
| if (e.status !== 'Active') { | |
| statuses['On Hold / Blocked']++; | |
| } else if (paidAmount >= e.totalAmount) { | |
| statuses['Paid Off']++; | |
| } else if (paidAmount > 0) { | |
| statuses['Active - In Progress']++; | |
| } else { | |
| statuses['Active - Unpaid']++; | |
| } | |
| }); | |
| if (statusChart) statusChart.destroy(); | |
| statusChart = new Chart(ctx, { | |
| type: 'doughnut', | |
| data: { | |
| labels: Object.keys(statuses), | |
| datasets: [{ | |
| data: Object.values(statuses), | |
| backgroundColor: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444'], | |
| borderColor: '#ffffff', | |
| borderWidth: 2 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| plugins: { legend: { position: 'bottom', labels: { boxWidth: 12 } } } | |
| } | |
| }); | |
| }; | |
| const renderTable = () => { | |
| const searchTerm = searchInput.value.toLowerCase(); | |
| const filteredEntries = entries.filter(e => { | |
| const matchesTab = activeTab === 'all' || e.type === activeTab; | |
| if (!matchesTab) return false; | |
| const searchCorpus = [e.userName, e.mac, e.deviceName, e.loanPurpose, e.phone, e.address].join(' ').toLowerCase(); | |
| return !searchTerm || searchCorpus.includes(searchTerm); | |
| }); | |
| entriesTable.innerHTML = ''; | |
| emptyState.classList.toggle('hidden', filteredEntries.length > 0); | |
| filteredEntries.forEach(entry => { /* ... row rendering logic, same as previous ... */ }); | |
| // Full row rendering logic from previous version for brevity | |
| filteredEntries.forEach(entry => { | |
| const paidAmount = entry.payments.reduce((sum, p) => sum + p.amount, 0); | |
| const paidPercent = entry.totalAmount > 0 ? (paidAmount / entry.totalAmount) * 100 : 0; | |
| const isDevice = entry.type === 'device'; | |
| const statusClass = `status-badge ${entry.status.toLowerCase().replace(/[\s/]+/g, '-')}-badge`; | |
| const row = document.createElement('tr'); | |
| row.className = 'hover:bg-gray-50'; | |
| row.innerHTML = ` | |
| <td class="p-3"> | |
| <div class="flex items-center gap-3"> | |
| <i class="fas ${isDevice ? 'fa-wifi text-primary' : 'fa-hand-holding-usd text-secondary'} fa-lg"></i> | |
| <div> | |
| <div class="font-bold text-gray-800">${entry.userName}</div> | |
| <div class="text-gray-500">${isDevice ? (entry.mac || 'N/A') : (entry.loanPurpose || 'N/A')}</div> | |
| </div> | |
| </div> | |
| </td> | |
| <td class="p-3"> | |
| <div class="w-full bg-gray-200 rounded-full h-2.5"> | |
| <div class="bg-green-500 h-2.5 rounded-full" style="width: ${paidPercent}%"></div> | |
| </div> | |
| <div class="text-xs text-gray-600 mt-1 flex justify-between"> | |
| <span class="text-green-600 font-medium">Paid: ${paidPercent.toFixed(0)}%</span> | |
| <span class="text-red-600 font-medium">Due: ${(100 - paidPercent).toFixed(0)}%</span> | |
| </div> | |
| </td> | |
| <td class="p-3"><span class="${statusClass}">${entry.status}</span></td> | |
| <td class="p-3 text-right"> | |
| <div class="inline-flex space-x-2"> | |
| <button class="action-btn" data-action="payment" data-id="${entry.id}" title="Manage Payments"><i class="fas fa-money-check-alt"></i></button> | |
| <button class="action-btn" data-action="edit" data-id="${entry.id}" title="Edit Entry"><i class="fas fa-edit"></i></button> | |
| <button class="action-btn" data-action="delete" data-id="${entry.id}" title="Delete Entry"><i class="fas fa-trash"></i></button> | |
| </div> | |
| </td> | |
| `; | |
| entriesTable.appendChild(row); | |
| }); | |
| }; | |
| const updateTabCounts = () => { | |
| document.getElementById('count-all').textContent = entries.length; | |
| document.getElementById('count-device').textContent = entries.filter(e => e.type === 'device').length; | |
| document.getElementById('count-loan').textContent = entries.filter(e => e.type === 'loan').length; | |
| }; | |
| // --- MODAL & FORM LOGIC --- | |
| const openModal = (html) => { | |
| modalContainer.innerHTML = html; | |
| const form = modalContainer.querySelector('form'); | |
| if(form) { | |
| const typeSelect = form.querySelector('select[name="type"]'); | |
| const toggle = () => { | |
| const isDevice = typeSelect.value === 'device'; | |
| form.querySelector('.device-fields').style.display = isDevice ? 'grid' : 'none'; | |
| form.querySelector('.loan-fields').style.display = isDevice ? 'none' : 'block'; | |
| }; | |
| typeSelect.addEventListener('change', toggle); | |
| toggle(); | |
| } | |
| }; | |
| const closeModal = () => modalContainer.innerHTML = ''; | |
| const openAddModal = () => openModal(modalTemplate('add')); // modalTemplate is the same as previous version | |
| const openEditModal = (id) => { | |
| const entry = entries.find(e => e.id == id); | |
| openModal(modalTemplate('edit', entry)); // modalTemplate is the same as previous version | |
| }; | |
| const openPaymentModal = (id) => { /* ... same as previous version ... */ }; | |
| // --- CRUD & ACTIONS --- | |
| const handleFormSubmit = async (e) => { /* ... same as previous version ... */ }; | |
| const deleteEntry = (id) => { | |
| if (confirm('Delete this entry and all its history? This is irreversible.')) { | |
| entries = entries.filter(e => e.id != id); | |
| saveData(); | |
| updateUI(); | |
| showToast('Entry deleted.', 'success'); | |
| } | |
| }; | |
| const addPayment = (e) => { | |
| e.preventDefault(); | |
| const form = e.target; | |
| const id = form.dataset.id; | |
| const entry = entries.find(e => e.id == id); | |
| const amount = parseFloat(form.elements.amount.value); | |
| if (!entry || isNaN(amount) || amount <= 0) { | |
| showToast('Invalid amount.', 'error'); | |
| return; | |
| } | |
| entry.payments.push({ id: Date.now(), date: new Date().toISOString(), amount }); | |
| saveData(); | |
| openPaymentModal(id); // Re-open the modal to refresh it | |
| updateUI(); | |
| showToast('Payment added successfully!', 'success'); | |
| }; | |
| // --- EVENT DELEGATION --- | |
| document.addEventListener('click', (e) => { | |
| const target = e.target; | |
| // Modal closing | |
| if (target.matches('.modal-overlay, .action-btn-close, .action-btn-close i')) closeModal(); | |
| // Open "Add" modal | |
| if (target.closest('#open-add-modal-btn')) openAddModal(); | |
| // Table actions | |
| const actionBtn = target.closest('button[data-action]'); | |
| if (actionBtn && actionBtn.closest('tbody')) { | |
| const { action, id } = actionBtn.dataset; | |
| if (action === 'edit') openEditModal(id); | |
| if (action === 'delete') deleteEntry(id); | |
| if (action === 'payment') openPaymentModal(id); | |
| } | |
| // Tab switching | |
| const tabBtn = target.closest('.tab-button'); | |
| if (tabBtn) { | |
| document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); | |
| tabBtn.classList.add('active'); | |
| activeTab = tabBtn.dataset.tab; | |
| renderTable(); | |
| } | |
| }); | |
| document.addEventListener('submit', (e) => { | |
| if (e.target.matches('#add-form, #edit-form')) handleFormSubmit(e); | |
| if (e.target.matches('#add-payment-form')) addPayment(e); // *** FIX: Delegated payment submission | |
| }); | |
| searchInput.addEventListener('input', renderTable); | |
| // Add exportBtn listener here if needed | |
| // --- INITIALIZATION --- | |
| const initializeSampleData = () => { | |
| if (localStorage.getItem('loanAndDeviceApp_v3')) return; | |
| entries = [ | |
| { id: 1, type: 'loan', userName: 'Shehroz Ali', totalAmount: 500000, installmentDay: 22, installmentAmount: 20000, phone: '0300-1234567', address: '123 Main St, Karachi', loanPurpose: 'Car Purchase', payments: [{id: 101, date: new Date().toISOString(), amount: 50000, isInitial: true}, {id: 102, date: new Date().toISOString(), amount: 20000}], documents: [], addedTime: new Date().toISOString(), status: 'Active' }, | |
| { id: 2, type: 'device', userName: 'Fatima Jilani', totalAmount: 15000, mac: 'AA:BB:CC:11:22:33', deviceName: 'Office Router', payments: [{id:201, date: new Date().toISOString(), amount: 15000, isInitial: true}], addedTime: new Date().toISOString(), status: 'Paid Off' }, | |
| { id: 3, type: 'loan', userName: 'Bilal Ahmed', totalAmount: 75000, installmentDay: 1, installmentAmount: 10000, phone: '', address: '', loanPurpose: 'Business Startup', payments: [], documents: [], addedTime: new Date().toISOString(), status: 'Active' }, | |
| { id: 4, type: 'device', userName: 'Guest Wifi', totalAmount: 8000, mac: '11:22:33:AA:BB:CC', deviceName: 'Lobby Access Point', payments: [], addedTime: new Date().toISOString(), status: 'Blocked' } | |
| ]; | |
| saveData(); | |
| }; | |
| // --- Full function definitions needed by the slimmed down code above --- | |
| // (These are complex and kept here for readability) | |
| const paymentModalLogic = { | |
| open: (id) => { | |
| const entry = entries.find(e => e.id == id); | |
| if (!entry) return; | |
| const paidAmount = entry.payments.reduce((sum, p) => sum + p.amount, 0); | |
| let nextPaymentDateStr = 'N/A', totalInstallments = 'N/A'; | |
| if (entry.installmentDay && entry.installmentAmount > 0) { | |
| totalInstallments = Math.ceil(entry.totalAmount / entry.installmentAmount); | |
| const today = new Date(); | |
| let nextDate = new Date(today.getFullYear(), today.getMonth(), entry.installmentDay); | |
| if (today.getDate() > entry.installmentDay) nextDate.setMonth(nextDate.getMonth() + 1); | |
| nextPaymentDateStr = nextDate.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }); | |
| } | |
| let docsHtml = '<p class="text-sm text-gray-500">No documents attached.</p>'; | |
| if (entry.documents && entry.documents.length > 0) { | |
| docsHtml = entry.documents.map(doc => `<a href="${doc.data}" download="${doc.name}" class="text-indigo-600 hover:underline flex items-center gap-2"><i class="fas fa-file-alt"></i> ${doc.name}</a>`).join(''); | |
| } | |
| const modalHtml = ` | |
| <div class="modal-overlay active" id="payment-modal-overlay"> | |
| <div class="modal-content"> | |
| <div class="p-6 space-y-4"> | |
| <div class="flex justify-between items-center"><h3 class="text-xl font-bold text-gray-800">Payment Details</h3><button type="button" class="action-btn-close"><i class="fas fa-times"></i></button></div> | |
| <div class="grid grid-cols-2 gap-4 text-sm p-4 bg-gray-50 rounded-lg"> | |
| <div><p class="text-gray-500">User</p><p class="font-semibold">${entry.userName}</p></div> | |
| ${entry.phone ? `<div><p class="text-gray-500">Phone</p><p class="font-semibold">${entry.phone}</p></div>` : ''} | |
| ${entry.address ? `<div class="col-span-2"><p class="text-gray-500">Address</p><p class="font-semibold">${entry.address}</p></div>` : ''} | |
| </div> | |
| <div class="grid grid-cols-3 gap-4 text-center"> | |
| <div class="p-2 rounded-lg bg-indigo-50"><p class="text-sm">Total Loan</p><p class="font-bold">${formatCurrency(entry.totalAmount)}</p></div> | |
| <div class="p-2 rounded-lg bg-green-50"><p class="text-sm">Paid</p><p class="font-bold text-green-600">${formatCurrency(paidAmount)}</p></div> | |
| <div class="p-2 rounded-lg bg-red-50"><p class="text-sm">Remaining</p><p class="font-bold text-red-600">${formatCurrency(entry.totalAmount - paidAmount)}</p></div> | |
| </div> | |
| <div class="border-t pt-4 space-y-3"><h4 class="font-semibold">Installment Details</h4><div class="grid grid-cols-3 gap-4 text-sm text-center"><div><p class="text-gray-500">Next Payment</p><p class="font-semibold">${nextPaymentDateStr}</p></div><div><p class="text-gray-500">Installment</p><p class="font-semibold">${formatCurrency(entry.installmentAmount)}</p></div><div><p class="text-gray-500">Total Installments</p><p class="font-semibold">${totalInstallments}</p></div></div></div> | |
| <div class="border-t pt-4 space-y-2"><h4 class="font-semibold">Documents</h4>${docsHtml}</div> | |
| <div class="border-t pt-4"><h4 class="font-semibold mb-2">Payment History</h4><div class="max-h-32 overflow-y-auto pr-2" id="payment-history-list"></div></div> | |
| <form id="add-payment-form" data-id="${id}" class="flex gap-3 pt-4 border-t"><input type="number" name="amount" class="form-input flex-grow" placeholder="Amount (Rs.)" required><button type="submit" class="btn bg-green-600 text-white hover:bg-green-700">Add</button></form> | |
| </div></div></div>`; | |
| openModal(modalHtml); | |
| this.renderHistory(entry); | |
| }, | |
| renderHistory: (entry) => { | |
| const list = document.getElementById('payment-history-list'); list.innerHTML = ''; | |
| if (entry.payments.length === 0) { list.innerHTML = '<p class="text-sm text-gray-500">No payments recorded.</p>'; return; } | |
| [...entry.payments].reverse().forEach(p => { | |
| const item = document.createElement('div'); | |
| item.className = 'text-sm flex justify-between items-center p-2 rounded hover:bg-gray-100'; | |
| item.innerHTML = `<div><p class="font-semibold">${formatCurrency(p.amount)}</p><p class="text-xs text-gray-500">${new Date(p.date).toLocaleString()}</p></div>${p.isInitial ? '<span class="text-xs font-bold text-blue-600">DOWN PAYMENT</span>' : ''}`; | |
| list.appendChild(item); | |
| }); | |
| } | |
| }; | |
| openPaymentModal = paymentModalLogic.open.bind(paymentModalLogic); | |
| handleFormSubmit = async (e) => { | |
| e.preventDefault(); | |
| const form = e.target; const isEdit = form.id === 'edit-form'; const id = isEdit ? form.dataset.id : Date.now(); | |
| const formData = new FormData(form); const data = Object.fromEntries(formData.entries()); | |
| let entry = isEdit ? entries.find(e => e.id == id) : { id, payments: [], documents: [] }; | |
| const paidAmount = isEdit ? entry.payments.reduce((sum, p) => sum + p.amount, 0) : 0; | |
| const downPayment = isEdit ? (entry.payments.find(p => p.isInitial)?.amount || 0) : 0; | |
| if (parseFloat(data.totalAmount) < paidAmount - downPayment) { showToast('Total amount cannot be less than payments already made.', 'error'); return; } | |
| Object.assign(entry, { | |
| type: data.type, userName: data.userName, totalAmount: parseFloat(data.totalAmount), | |
| mac: data.mac || null, deviceName: data.deviceName || null, loanPurpose: data.loanPurpose || null, | |
| phone: data.phone || null, address: data.address || null, | |
| installmentDay: data.installmentDay ? parseInt(data.installmentDay) : null, | |
| installmentAmount: data.installmentAmount ? parseFloat(data.installmentAmount) : null, | |
| status: entry.status || 'Active' | |
| }); | |
| const doc = await getFileAsBase64(data.document); if (doc) entry.documents = [doc]; | |
| if (!isEdit) { | |
| const initialPayment = parseFloat(data.initialPayment) || 0; | |
| if (initialPayment > 0) entry.payments.push({ id: Date.now(), date: new Date().toISOString(), amount: initialPayment, isInitial: true }); | |
| entry.addedTime = new Date().toISOString(); entries.unshift(entry); | |
| } | |
| saveData(); updateUI(); closeModal(); showToast(`Entry ${isEdit ? 'updated' : 'added'}!`, 'success'); | |
| }; | |
| const modalTemplateText = ` | |
| <div class="modal-overlay active" id="form-modal-overlay"> | |
| <div class="modal-content"> | |
| <form id="{formId}" data-id="{id}"> | |
| <div class="p-6"> | |
| <div class="flex justify-between items-center mb-6"><h3 class="text-xl font-bold text-gray-800">{title}</h3><button type="button" class="action-btn-close"><i class="fas fa-times"></i></button></div> | |
| <div class="space-y-4"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div><label class="font-medium text-sm">Entry Type</label><select name="type" class="form-input mt-1" {disabled}><option value="device" {deviceSelected}>Device</option><option value="loan" {loanSelected}>Loan</option></select></div> | |
| <div><label class="font-medium text-sm">User Name</label><input type="text" name="userName" class="form-input mt-1" value="{userName}" required></div> | |
| </div> | |
| <div class="form-section device-fields grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div><label class="font-medium text-sm">MAC Address</label><input type="text" name="mac" class="form-input mt-1" value="{mac}"></div> | |
| <div><label class="font-medium text-sm">Device Name</label><input type="text" name="deviceName" class="form-input mt-1" value="{deviceName}"></div> | |
| </div> | |
| <div class="form-section loan-fields space-y-4"> | |
| <div><label class="font-medium text-sm">Loan Purpose</label><input type="text" name="loanPurpose" class="form-input mt-1" value="{loanPurpose}"></div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div><label class="font-medium text-sm">Phone (Optional)</label><input type="tel" name="phone" class="form-input mt-1" value="{phone}"></div> | |
| <div><label class="font-medium text-sm">Address (Optional)</label><input type="text" name="address" class="form-input mt-1" value="{address}"></div> | |
| </div> | |
| <div><label class="font-medium text-sm">Document (Optional)</label><input type="file" name="document" class="form-input mt-1"></div> | |
| </div> | |
| <div class="border-t pt-4 space-y-4"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div><label class="font-medium text-sm">Total Loan (Rs.)</label><input type="number" name="totalAmount" class="form-input mt-1" value="{totalAmount}" required min="1"></div> | |
| <div><label class="font-medium text-sm">{initialPaymentLabel} (Rs.)</label><input type="number" name="initialPayment" class="form-input mt-1" value="{initialPayment}" min="0" {initialPaymentDisabled}></div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div><label class="font-medium text-sm">Installment Day (1-31)</label><input type="number" name="installmentDay" class="form-input mt-1" min="1" max="31" value="{installmentDay}"></div> | |
| <div><label class="font-medium text-sm">Installment Amount (Rs.)</label><input type="number" name="installmentAmount" class="form-input mt-1" min="1" value="{installmentAmount}"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-4 bg-gray-50 text-right"><button type="submit" class="btn btn-primary">{btnText}</button></div> | |
| </form> | |
| </div> | |
| </div>`; | |
| const _modalTemplate = (type, entry = {}) => { | |
| const isEdit = type === 'edit'; | |
| return modalTemplateText | |
| .replace('{formId}', isEdit ? 'edit-form' : 'add-form') | |
| .replace('{id}', entry.id || '') | |
| .replace('{title}', isEdit ? 'Edit Entry' : 'Add New Entry') | |
| .replace('{disabled}', isEdit ? 'disabled' : '') | |
| .replace('{deviceSelected}', (entry.type || 'device') === 'device' ? 'selected' : '') | |
| .replace('{loanSelected}', entry.type === 'loan' ? 'selected' : '') | |
| .replace('{userName}', entry.userName || '') | |
| .replace('{mac}', entry.mac || '') | |
| .replace('{deviceName}', entry.deviceName || '') | |
| .replace('{loanPurpose}', entry.loanPurpose || '') | |
| .replace('{phone}', entry.phone || '') | |
| .replace('{address}', entry.address || '') | |
| .replace('{totalAmount}', entry.totalAmount || '') | |
| .replace('{initialPaymentLabel}', isEdit ? 'Current Down Payment' : 'Down Payment') | |
| .replace('{initialPayment}', isEdit ? (entry.payments?.find(p => p.isInitial)?.amount || 0) : '') | |
| .replace('{initialPaymentDisabled}', isEdit ? 'disabled' : '') | |
| .replace('{installmentDay}', entry.installmentDay || '') | |
| .replace('{installmentAmount}', entry.installmentAmount || '') | |
| .replace('{btnText}', isEdit ? 'Save Changes' : 'Add Entry'); | |
| }; | |
| modalTemplate = _modalTemplate; | |
| initializeSampleData(); | |
| updateUI(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |