offerpk3 commited on
Commit
c9b19de
·
verified ·
1 Parent(s): 0c5073c

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +316 -319
index.html CHANGED
@@ -3,93 +3,117 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Advanced Loan & Device Management System</title>
7
  <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
 
9
  <style>
10
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
11
  :root {
12
  --primary: #4f46e5; --secondary: #db2777; --success: #10b981;
13
  --warning: #f59e0b; --danger: #ef4444; --info: #3b82f6;
14
  }
15
- body { font-family: 'Inter', sans-serif; background: #f1f5f9; }
16
  .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); }
17
  .status-badge { padding: 4px 12px; border-radius: 20px; font-size: 11px; font-weight: 600; text-transform: uppercase; }
18
  .active-badge { background-color: #ecfdf5; color: #065f46; }
19
- .blocked-badge, .default-badge, .on-hold-badge { background-color: #fef2f2; color: #991b1b; }
20
- .paid-badge { background-color: #dcfce7; color: #166534; }
21
  .partial-badge { background-color: #fefce8; color: #854d0e; }
22
  .unpaid-badge { background-color: #fee2e2; color: #991b1b; }
23
  .form-input { border: 1px solid #e2e8f0; border-radius: 10px; padding: 10px 14px; width: 100%; background-color: #f8fafc; }
24
  .form-input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15); outline: none; }
25
  .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); }
26
  .toast.show { transform: translateY(0); opacity: 1; }
27
- .toast.success { background: var(--success); } .toast.error { background: var(--danger); }
28
- .toast.info { background: var(--info); } .toast.warning { background: var(--warning); }
29
  .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; }
30
- .btn-primary { background: var(--primary); color: white; }
31
- .btn-primary:hover { background: #4338ca; }
32
  .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; }
33
  .modal-overlay.active { opacity: 1; pointer-events: all; }
34
  .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; }
35
  .modal-overlay.active .modal-content { transform: scale(1); opacity: 1; }
36
- .tab-button { padding: 10px 20px; border-radius: 8px; font-weight: 600; border: 2px solid transparent; }
37
  .tab-button.active { color: var(--primary); background-color: #eef2ff; }
38
  </style>
39
  </head>
40
- <body class="min-h-screen p-4 md:p-8">
41
  <div id="toast-container" class="fixed bottom-0 right-0 p-8 space-y-3 z-50"></div>
42
-
43
- <!-- Modals Container -->
44
  <div id="modal-container"></div>
45
 
46
- <div class="max-w-7xl mx-auto">
47
- <div class="flex flex-col md:flex-row justify-between items-center mb-6">
 
48
  <div>
49
- <h1 class="text-3xl font-bold text-gray-800">Advanced Management System</h1>
50
- <p class="text-gray-600 mt-1">Manage Loans, Devices, Payments, and Documents.</p>
51
  </div>
52
- <button id="open-add-modal-btn" class="btn btn-primary mt-4 md:mt-0">
53
  <i class="fas fa-plus-circle mr-2"></i> Add New Entry
54
  </button>
55
  </div>
56
 
57
- <div class="grid grid-cols-1">
58
- <div class="card p-4 sm:p-6">
59
- <div class="flex flex-col md:flex-row justify-between items-center mb-4 border-b pb-4 gap-4">
60
- <div class="flex-wrap flex space-x-2">
61
- <button class="tab-button active" data-tab="all">All (<span id="count-all">0</span>)</button>
62
- <button class="tab-button" data-tab="device">Devices (<span id="count-device">0</span>)</button>
63
- <button class="tab-button" data-tab="loan">Loans (<span id="count-loan">0</span>)</button>
64
- </div>
65
- <div class="flex space-x-3 w-full md:w-auto">
66
- <div class="relative flex-grow">
67
- <input type="text" id="search-input" class="form-input pl-10 w-full" placeholder="Search...">
68
- <i class="fas fa-search absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
69
- </div>
70
- <button id="export-btn" class="btn bg-gray-700 hover:bg-gray-800 text-white" title="Export as CSV">
71
- <i class="fas fa-file-csv"></i>
72
- </button>
73
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  </div>
75
-
76
- <div class="overflow-x-auto">
77
- <table class="w-full text-sm">
78
- <thead>
79
- <tr class="text-left text-gray-500 font-medium border-b-2 border-gray-200">
80
- <th class="p-3">User / Details</th>
81
- <th class="p-3">Loan Progress</th>
82
- <th class="p-3">Status</th>
83
- <th class="p-3 text-right">Actions</th>
84
- </tr>
85
- </thead>
86
- <tbody id="entries-table" class="divide-y divide-gray-100"></tbody>
87
- </table>
88
- <div id="empty-state" class="py-16 text-center hidden">
89
- <i class="fas fa-inbox text-5xl text-gray-300 mb-4"></i>
90
- <h3 class="text-lg font-medium text-gray-700">No Entries Found</h3>
91
- <p class="text-gray-500 mt-1">Click "Add New Entry" to get started.</p>
92
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  </div>
94
  </div>
95
  </div>
@@ -97,100 +121,101 @@
97
 
98
  <script>
99
  document.addEventListener('DOMContentLoaded', () => {
100
- // --- STATE MANAGEMENT ---
101
- let entries = JSON.parse(localStorage.getItem('loanAndDeviceApp_v2')) || [];
102
  let activeTab = 'all';
 
103
 
104
  // --- DOM ELEMENTS ---
 
105
  const entriesTable = document.getElementById('entries-table');
106
  const emptyState = document.getElementById('empty-state');
107
  const searchInput = document.getElementById('search-input');
108
- const exportBtn = document.getElementById('export-btn');
109
- const openAddModalBtn = document.getElementById('open-add-modal-btn');
110
- const modalContainer = document.getElementById('modal-container');
111
 
112
- // --- TEMPLATES ---
113
- const modalTemplate = (type, entry = {}) => {
114
- const isEdit = type === 'edit';
115
- const title = isEdit ? 'Edit Entry' : 'Add New Entry';
116
- const btnText = isEdit ? 'Save Changes' : 'Add Entry';
117
- const formId = isEdit ? 'edit-form' : 'add-form';
118
- const entryType = entry.type || 'device';
119
-
120
- return `
121
- <div class="modal-overlay active" id="${formId}-modal-overlay">
122
- <div class="modal-content">
123
- <form id="${formId}" data-id="${entry.id || ''}">
124
- <div class="p-6">
125
- <div class="flex justify-between items-center mb-6">
126
- <h3 class="text-xl font-bold text-gray-800">${title}</h3>
127
- <button type="button" class="action-btn-close"><i class="fas fa-times"></i></button>
128
- </div>
129
- <div class="space-y-4">
130
- <!-- Common Fields -->
131
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
132
- <div>
133
- <label class="font-medium text-sm">Entry Type</label>
134
- <select name="type" class="form-input mt-1" ${isEdit ? 'disabled' : ''}>
135
- <option value="device" ${entryType === 'device' ? 'selected' : ''}>Network Device</option>
136
- <option value="loan" ${entryType === 'loan' ? 'selected' : ''}>Standalone Loan</option>
137
- </select>
138
- </div>
139
- <div>
140
- <label for="userName" class="font-medium text-sm">User Name</label>
141
- <input type="text" name="userName" class="form-input mt-1" value="${entry.userName || ''}" required>
142
- </div>
143
- </div>
144
-
145
- <!-- Device Specific -->
146
- <div class="form-section device-fields grid grid-cols-1 md:grid-cols-2 gap-4">
147
- <div><label class="font-medium text-sm">MAC Address</label><input type="text" name="mac" class="form-input mt-1" value="${entry.mac || ''}"></div>
148
- <div><label class="font-medium text-sm">Device Name</label><input type="text" name="deviceName" class="form-input mt-1" value="${entry.deviceName || ''}"></div>
149
- </div>
150
-
151
- <!-- Loan Specific -->
152
- <div class="form-section loan-fields space-y-4">
153
- <div><label class="font-medium text-sm">Loan Purpose</label><input type="text" name="loanPurpose" class="form-input mt-1" value="${entry.loanPurpose || ''}"></div>
154
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
155
- <div><label class="font-medium text-sm">Phone (Optional)</label><input type="tel" name="phone" class="form-input mt-1" value="${entry.phone || ''}"></div>
156
- <div><label class="font-medium text-sm">Address (Optional)</label><input type="text" name="address" class="form-input mt-1" value="${entry.address || ''}"></div>
157
- </div>
158
- <div><label class="font-medium text-sm">Document (Optional)</label><input type="file" name="document" class="form-input mt-1"></div>
159
- </div>
160
-
161
- <!-- Financials -->
162
- <div class="border-t pt-4 space-y-4">
163
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
164
- <div><label class="font-medium text-sm">Total Loan (Rs.)</label><input type="number" name="totalAmount" class="form-input mt-1" value="${entry.totalAmount || ''}" required min="1"></div>
165
- <div><label class="font-medium text-sm">${isEdit ? 'Current Down Payment' : 'Down Payment'} (Rs.)</label><input type="number" name="initialPayment" class="form-input mt-1" value="${isEdit ? (entry.payments.find(p => p.isInitial)?.amount || 0) : ''}" min="0" ${isEdit ? 'disabled' : ''}></div>
166
- </div>
167
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
168
- <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="${entry.installmentDay || ''}"></div>
169
- <div><label class="font-medium text-sm">Installment Amount (Rs.)</label><input type="number" name="installmentAmount" class="form-input mt-1" min="1" value="${entry.installmentAmount || ''}"></div>
170
- </div>
171
- </div>
172
- </div>
173
- </div>
174
- <div class="p-4 bg-gray-50 text-right"><button type="submit" class="btn btn-primary">${btnText}</button></div>
175
- </form>
176
- </div>
177
- </div>`;
178
- };
179
- const paymentModalTemplate = (entry) => { /* ... see implementation below ... */ };
180
 
181
  // --- HELPER FUNCTIONS ---
182
- const saveData = () => localStorage.setItem('loanAndDeviceApp_v2', JSON.stringify(entries));
183
  const formatCurrency = (amount) => `Rs.${(amount || 0).toLocaleString('en-IN')}`;
184
- const showToast = (message, type = 'info') => { /* implementation unchanged */ };
185
-
186
  const getFileAsBase64 = (file) => new Promise((resolve) => {
187
  if (!file) { resolve(null); return; }
188
  const reader = new FileReader();
189
  reader.readAsDataURL(file);
190
  reader.onload = () => resolve({ name: file.name, data: reader.result });
191
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
 
193
- // --- RENDER & UI LOGIC ---
194
  const renderTable = () => {
195
  const searchTerm = searchInput.value.toLowerCase();
196
  const filteredEntries = entries.filter(e => {
@@ -199,22 +224,15 @@
199
  const searchCorpus = [e.userName, e.mac, e.deviceName, e.loanPurpose, e.phone, e.address].join(' ').toLowerCase();
200
  return !searchTerm || searchCorpus.includes(searchTerm);
201
  });
202
-
203
  entriesTable.innerHTML = '';
204
  emptyState.classList.toggle('hidden', filteredEntries.length > 0);
205
-
 
206
  filteredEntries.forEach(entry => {
207
  const paidAmount = entry.payments.reduce((sum, p) => sum + p.amount, 0);
208
- const remainingAmount = entry.totalAmount - paidAmount;
209
- const paidPercent = (paidAmount / entry.totalAmount) * 100;
210
-
211
- let paymentStatus, paymentStatusClass;
212
- if (paidAmount >= entry.totalAmount) { paymentStatus = 'Paid Off'; paymentStatusClass = 'paid-badge'; }
213
- else if (paidAmount > 0) { paymentStatus = 'Partial'; paymentStatusClass = 'partial-badge'; }
214
- else { paymentStatus = 'Unpaid'; paymentStatusClass = 'unpaid-badge'; }
215
-
216
  const isDevice = entry.type === 'device';
217
- const statusClass = `status-badge ${entry.status.toLowerCase().replace(' ','-')}-badge`;
218
 
219
  const row = document.createElement('tr');
220
  row.className = 'hover:bg-gray-50';
@@ -224,7 +242,7 @@
224
  <i class="fas ${isDevice ? 'fa-wifi text-primary' : 'fa-hand-holding-usd text-secondary'} fa-lg"></i>
225
  <div>
226
  <div class="font-bold text-gray-800">${entry.userName}</div>
227
- <div class="text-gray-500">${isDevice ? entry.mac : entry.loanPurpose}</div>
228
  </div>
229
  </div>
230
  </td>
@@ -248,15 +266,17 @@
248
  `;
249
  entriesTable.appendChild(row);
250
  });
251
- updateCounts();
252
  };
253
- const updateCounts = () => { /* updates counts in tabs */ };
254
 
255
- // --- MODAL HANDLING ---
 
 
 
 
 
 
256
  const openModal = (html) => {
257
- closeAllModals();
258
  modalContainer.innerHTML = html;
259
- // Add dynamic form logic
260
  const form = modalContainer.querySelector('form');
261
  if(form) {
262
  const typeSelect = form.querySelector('select[name="type"]');
@@ -269,76 +289,52 @@
269
  toggle();
270
  }
271
  };
272
- const closeAllModals = () => modalContainer.innerHTML = '';
273
-
274
- const openAddModal = () => openModal(modalTemplate('add'));
275
  const openEditModal = (id) => {
276
  const entry = entries.find(e => e.id == id);
277
- openModal(modalTemplate('edit', entry));
278
  };
279
- const openPaymentModal = (id) => { /* implementation below */ };
280
 
281
  // --- CRUD & ACTIONS ---
282
- const handleFormSubmit = async (e) => {
283
- e.preventDefault();
284
- const form = e.target;
285
- const isEdit = form.id === 'edit-form';
286
- const id = isEdit ? form.dataset.id : Date.now();
287
-
288
- const formData = new FormData(form);
289
- const data = Object.fromEntries(formData.entries());
290
-
291
- let entry = isEdit ? entries.find(e => e.id == id) : { id, payments: [], documents: [] };
292
-
293
- Object.assign(entry, {
294
- type: data.type,
295
- userName: data.userName,
296
- totalAmount: parseFloat(data.totalAmount),
297
- mac: data.mac || null,
298
- deviceName: data.deviceName || null,
299
- loanPurpose: data.loanPurpose || null,
300
- phone: data.phone || null,
301
- address: data.address || null,
302
- installmentDay: data.installmentDay ? parseInt(data.installmentDay) : null,
303
- installmentAmount: data.installmentAmount ? parseFloat(data.installmentAmount) : null,
304
- status: entry.status || 'Active'
305
- });
306
-
307
- const doc = await getFileAsBase64(data.document);
308
- if (doc) entry.documents = [doc]; // Simple overwrite for now
309
-
310
- if (!isEdit) {
311
- const initialPayment = parseFloat(data.initialPayment) || 0;
312
- if (initialPayment > 0) {
313
- entry.payments.push({ id: Date.now(), date: new Date().toISOString(), amount: initialPayment, isInitial: true });
314
- }
315
- entry.addedTime = new Date().toISOString();
316
- entries.unshift(entry);
317
- }
318
-
319
- saveData();
320
- renderTable();
321
- closeAllModals();
322
- showToast(`Entry ${isEdit ? 'updated' : 'added'} successfully!`, 'success');
323
- };
324
-
325
  const deleteEntry = (id) => {
326
  if (confirm('Delete this entry and all its history? This is irreversible.')) {
327
  entries = entries.filter(e => e.id != id);
328
  saveData();
329
- renderTable();
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  }
 
 
 
 
 
 
331
  };
332
 
333
- // --- EVENT LISTENERS ---
334
- openAddModalBtn.addEventListener('click', openAddModal);
335
  document.addEventListener('click', (e) => {
 
336
  // Modal closing
337
- if (e.target.matches('.modal-overlay, .action-btn-close, .action-btn-close i')) {
338
- closeAllModals();
339
- }
340
  // Table actions
341
- const actionBtn = e.target.closest('button[data-action]');
342
  if (actionBtn && actionBtn.closest('tbody')) {
343
  const { action, id } = actionBtn.dataset;
344
  if (action === 'edit') openEditModal(id);
@@ -346,7 +342,7 @@
346
  if (action === 'payment') openPaymentModal(id);
347
  }
348
  // Tab switching
349
- const tabBtn = e.target.closest('.tab-button');
350
  if (tabBtn) {
351
  document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
352
  tabBtn.classList.add('active');
@@ -354,170 +350,171 @@
354
  renderTable();
355
  }
356
  });
 
357
  document.addEventListener('submit', (e) => {
358
- if (e.target.matches('#add-form, #edit-form')) {
359
- handleFormSubmit(e);
360
- }
361
- // Add payment form is handled inside its modal logic
362
  });
363
 
364
- // --- INITIALIZATION ---
365
- const initializeSampleData = () => { /* ... */ };
366
- renderTable();
367
 
368
- // --- FULL IMPLEMENTATION OF COMPLEX FUNCTIONS ---
369
- // (to keep the above structure clean)
 
 
 
 
 
 
 
 
 
 
 
 
370
  const paymentModalLogic = {
371
  open: (id) => {
372
  const entry = entries.find(e => e.id == id);
373
  if (!entry) return;
374
-
375
  const paidAmount = entry.payments.reduce((sum, p) => sum + p.amount, 0);
376
- const remaining = entry.totalAmount - paidAmount;
377
- const progress = (paidAmount / entry.totalAmount) * 100;
378
-
379
- // Installment calculations
380
- let nextPaymentDateStr = 'N/A';
381
- let totalInstallments = 'N/A';
382
  if (entry.installmentDay && entry.installmentAmount > 0) {
383
  totalInstallments = Math.ceil(entry.totalAmount / entry.installmentAmount);
384
  const today = new Date();
385
  let nextDate = new Date(today.getFullYear(), today.getMonth(), entry.installmentDay);
386
- if (today.getDate() > entry.installmentDay) {
387
- nextDate.setMonth(nextDate.getMonth() + 1);
388
- }
389
- nextPaymentDateStr = nextDate.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric'});
390
  }
391
-
392
- // Render documents
393
  let docsHtml = '<p class="text-sm text-gray-500">No documents attached.</p>';
394
  if (entry.documents && entry.documents.length > 0) {
395
- docsHtml = entry.documents.map(doc =>
396
- `<a href="${doc.data}" download="${doc.name}" class="text-indigo-600 hover:underline flex items-center gap-2">
397
- <i class="fas fa-file-alt"></i> ${doc.name}
398
- </a>`
399
- ).join('');
400
  }
401
-
402
- // Build modal HTML
403
  const modalHtml = `
404
  <div class="modal-overlay active" id="payment-modal-overlay">
405
  <div class="modal-content">
406
  <div class="p-6 space-y-4">
407
- <div class="flex justify-between items-center">
408
- <h3 class="text-xl font-bold text-gray-800">Payment Details</h3>
409
- <button type="button" class="action-btn-close"><i class="fas fa-times"></i></button>
410
- </div>
411
-
412
  <div class="grid grid-cols-2 gap-4 text-sm p-4 bg-gray-50 rounded-lg">
413
  <div><p class="text-gray-500">User</p><p class="font-semibold">${entry.userName}</p></div>
414
  ${entry.phone ? `<div><p class="text-gray-500">Phone</p><p class="font-semibold">${entry.phone}</p></div>` : ''}
415
  ${entry.address ? `<div class="col-span-2"><p class="text-gray-500">Address</p><p class="font-semibold">${entry.address}</p></div>` : ''}
416
  </div>
417
-
418
  <div class="grid grid-cols-3 gap-4 text-center">
419
  <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>
420
  <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>
421
- <div class="p-2 rounded-lg bg-red-50"><p class="text-sm">Remaining</p><p class="font-bold text-red-600">${formatCurrency(remaining)}</p></div>
422
- </div>
423
-
424
- <div class="border-t pt-4 space-y-3">
425
- <h4 class="font-semibold">Installment Details</h4>
426
- <div class="grid grid-cols-3 gap-4 text-sm text-center">
427
- <div><p class="text-gray-500">Next Payment</p><p class="font-semibold">${nextPaymentDateStr}</p></div>
428
- <div><p class="text-gray-500">Installment</p><p class="font-semibold">${formatCurrency(entry.installmentAmount)}</p></div>
429
- <div><p class="text-gray-500">Total Installments</p><p class="font-semibold">${totalInstallments}</p></div>
430
- </div>
431
- </div>
432
-
433
- <div class="border-t pt-4 space-y-2">
434
- <h4 class="font-semibold">Documents</h4>
435
- ${docsHtml}
436
  </div>
437
-
438
- <div class="border-t pt-4">
439
- <h4 class="font-semibold mb-2">Payment History</h4>
440
- <div class="max-h-32 overflow-y-auto pr-2" id="payment-history-list"></div>
441
- </div>
442
-
443
- <form id="add-payment-form" data-id="${id}" class="flex gap-3 pt-4 border-t">
444
- <input type="number" name="amount" class="form-input flex-grow" placeholder="Amount (Rs.)" required>
445
- <button type="submit" class="btn bg-green-600 text-white hover:bg-green-700">Add</button>
446
- </form>
447
- </div>
448
- </div>
449
- </div>`;
450
  openModal(modalHtml);
451
  this.renderHistory(entry);
452
-
453
- // Add submit listener for this specific form
454
- document.getElementById('add-payment-form').addEventListener('submit', this.addPayment);
455
  },
456
  renderHistory: (entry) => {
457
- const list = document.getElementById('payment-history-list');
458
- list.innerHTML = '';
459
- if (entry.payments.length === 0) {
460
- list.innerHTML = '<p class="text-sm text-gray-500">No payments recorded.</p>';
461
- return;
462
- }
463
  [...entry.payments].reverse().forEach(p => {
464
  const item = document.createElement('div');
465
  item.className = 'text-sm flex justify-between items-center p-2 rounded hover:bg-gray-100';
466
- item.innerHTML = `
467
- <div>
468
- <p class="font-semibold">${formatCurrency(p.amount)}</p>
469
- <p class="text-xs text-gray-500">${new Date(p.date).toLocaleString()}</p>
470
- </div>
471
- ${p.isInitial ? '<span class="text-xs font-bold text-blue-600">DOWN PAYMENT</span>' : ''}
472
- `;
473
  list.appendChild(item);
474
  });
475
- },
476
- addPayment: (e) => {
477
- e.preventDefault();
478
- const form = e.target;
479
- const id = form.dataset.id;
480
- const entry = entries.find(e => e.id == id);
481
- const amount = parseFloat(form.elements.amount.value);
482
-
483
- if (!entry || isNaN(amount) || amount <= 0) return;
484
-
485
- entry.payments.push({ id: Date.now(), date: new Date().toISOString(), amount });
486
- saveData();
487
- this.open(id); // Re-open the modal to refresh it
488
- renderTable();
489
  }
490
  };
491
  openPaymentModal = paymentModalLogic.open.bind(paymentModalLogic);
492
- updateCounts = () => {
493
- document.getElementById('count-all').textContent = entries.length;
494
- document.getElementById('count-device').textContent = entries.filter(e => e.type === 'device').length;
495
- document.getElementById('count-loan').textContent = entries.filter(e => e.type === 'loan').length;
496
- };
497
- showToast = (message, type = 'info') => {
498
- const container = document.getElementById('toast-container');
499
- const toast = document.createElement('div');
500
- const icons = { success: 'fa-check-circle', error: 'fa-times-circle', info: 'fa-info-circle', warning: 'fa-exclamation-triangle' };
501
- toast.className = `toast ${type} flex items-center space-x-3`;
502
- toast.innerHTML = `<i class="fas ${icons[type]}"></i><span>${message}</span>`;
503
- container.prepend(toast);
504
- setTimeout(() => toast.classList.add('show'), 10);
505
- setTimeout(() => {
506
- toast.classList.remove('show');
507
- toast.addEventListener('transitionend', () => toast.remove());
508
- }, 3000);
 
 
 
 
 
 
509
  };
510
- initializeSampleData = () => {
511
- if (localStorage.getItem('loanAndDeviceApp_v2')) return;
512
- entries = [
513
- { 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}], documents: [], addedTime: new Date().toISOString(), status: 'Active' },
514
- { id: 2, type: 'device', userName: 'Fatima Jilani', totalAmount: 15000, mac: 'AA:BB:CC:11:22:33', deviceName: 'Office Router', payments: [], addedTime: new Date().toISOString(), status: 'Active' }
515
- ];
516
- saveData();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  };
 
518
 
519
  initializeSampleData();
520
- renderTable();
521
  });
522
  </script>
523
  </body>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Admin Dashboard - Loan & Device Management</title>
7
  <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
10
  <style>
11
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
12
  :root {
13
  --primary: #4f46e5; --secondary: #db2777; --success: #10b981;
14
  --warning: #f59e0b; --danger: #ef4444; --info: #3b82f6;
15
  }
16
+ body { font-family: 'Inter', sans-serif; background-color: #f1f5f9; }
17
  .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); }
18
  .status-badge { padding: 4px 12px; border-radius: 20px; font-size: 11px; font-weight: 600; text-transform: uppercase; }
19
  .active-badge { background-color: #ecfdf5; color: #065f46; }
20
+ .blocked-badge, .on-hold-badge { background-color: #fefce8; color: #854d0e; }
21
+ .paid-off-badge { background-color: #dcfce7; color: #166534; }
22
  .partial-badge { background-color: #fefce8; color: #854d0e; }
23
  .unpaid-badge { background-color: #fee2e2; color: #991b1b; }
24
  .form-input { border: 1px solid #e2e8f0; border-radius: 10px; padding: 10px 14px; width: 100%; background-color: #f8fafc; }
25
  .form-input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15); outline: none; }
26
  .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); }
27
  .toast.show { transform: translateY(0); opacity: 1; }
 
 
28
  .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; }
 
 
29
  .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; }
30
  .modal-overlay.active { opacity: 1; pointer-events: all; }
31
  .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; }
32
  .modal-overlay.active .modal-content { transform: scale(1); opacity: 1; }
33
+ .tab-button { padding: 10px 20px; border-radius: 8px; font-weight: 600; }
34
  .tab-button.active { color: var(--primary); background-color: #eef2ff; }
35
  </style>
36
  </head>
37
+ <body class="p-4 md:p-8">
38
  <div id="toast-container" class="fixed bottom-0 right-0 p-8 space-y-3 z-50"></div>
 
 
39
  <div id="modal-container"></div>
40
 
41
+ <div class="max-w-7xl mx-auto space-y-8">
42
+ <!-- HEADER -->
43
+ <div class="flex flex-col md:flex-row justify-between items-center">
44
  <div>
45
+ <h1 class="text-3xl font-bold text-gray-800">Admin Dashboard</h1>
46
+ <p class="text-gray-600 mt-1">Overview of your loan and device portfolio.</p>
47
  </div>
48
+ <button id="open-add-modal-btn" class="btn bg-primary text-white mt-4 md:mt-0">
49
  <i class="fas fa-plus-circle mr-2"></i> Add New Entry
50
  </button>
51
  </div>
52
 
53
+ <!-- DASHBOARD STATS -->
54
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
55
+ <!-- Financial KPIs -->
56
+ <div class="card p-5 flex items-center space-x-4 col-span-1 md:col-span-2 lg:col-span-1">
57
+ <div class="bg-indigo-100 p-4 rounded-full"><i class="fas fa-landmark fa-2x text-indigo-600"></i></div>
58
+ <div>
59
+ <p class="text-gray-500">Total Loan Value</p>
60
+ <p class="text-2xl font-bold text-gray-800" id="stat-total-loan">Rs.0</p>
61
+ </div>
62
+ </div>
63
+ <div class="card p-5 flex items-center space-x-4">
64
+ <div class="bg-green-100 p-4 rounded-full"><i class="fas fa-check-double fa-2x text-green-600"></i></div>
65
+ <div>
66
+ <p class="text-gray-500">Total Collected</p>
67
+ <p class="text-2xl font-bold text-gray-800" id="stat-total-collected">Rs.0</p>
68
+ </div>
69
+ </div>
70
+ <div class="card p-5 flex items-center space-x-4">
71
+ <div class="bg-red-100 p-4 rounded-full"><i class="fas fa-file-invoice-dollar fa-2x text-red-600"></i></div>
72
+ <div>
73
+ <p class="text-gray-500">Total Outstanding</p>
74
+ <p class="text-2xl font-bold text-gray-800" id="stat-total-outstanding">Rs.0</p>
75
+ </div>
76
+ </div>
77
+ <!-- Chart -->
78
+ <div class="card p-5 lg:col-span-1 md:col-span-2 col-span-1">
79
+ <h3 class="font-bold text-gray-800 mb-2">Portfolio Status</h3>
80
+ <canvas id="status-chart"></canvas>
81
+ </div>
82
+ </div>
83
+
84
+ <!-- DATA TABLE -->
85
+ <div class="card p-4 sm:p-6">
86
+ <div class="flex flex-col md:flex-row justify-between items-center mb-4 border-b pb-4 gap-4">
87
+ <div class="flex-wrap flex space-x-2">
88
+ <button class="tab-button active" data-tab="all">All (<span id="count-all">0</span>)</button>
89
+ <button class="tab-button" data-tab="device">Devices (<span id="count-device">0</span>)</button>
90
+ <button class="tab-button" data-tab="loan">Loans (<span id="count-loan">0</span>)</button>
91
  </div>
92
+ <div class="flex space-x-3 w-full md:w-auto">
93
+ <div class="relative flex-grow">
94
+ <input type="text" id="search-input" class="form-input pl-10 w-full" placeholder="Search...">
95
+ <i class="fas fa-search absolute left-4 top-1/2 -translate-y-1/2 text-gray-400"></i>
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  </div>
97
+ <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>
98
+ </div>
99
+ </div>
100
+
101
+ <div class="overflow-x-auto">
102
+ <table class="w-full text-sm">
103
+ <thead>
104
+ <tr class="text-left text-gray-500 font-medium border-b-2 border-gray-200">
105
+ <th class="p-3">User / Details</th>
106
+ <th class="p-3">Loan Progress</th>
107
+ <th class="p-3">Status</th>
108
+ <th class="p-3 text-right">Actions</th>
109
+ </tr>
110
+ </thead>
111
+ <tbody id="entries-table" class="divide-y divide-gray-100"></tbody>
112
+ </table>
113
+ <div id="empty-state" class="py-16 text-center hidden">
114
+ <i class="fas fa-inbox text-5xl text-gray-300 mb-4"></i>
115
+ <h3 class="text-lg font-medium text-gray-700">No Entries Found</h3>
116
+ <p class="text-gray-500 mt-1">Click "Add New Entry" to get started.</p>
117
  </div>
118
  </div>
119
  </div>
 
121
 
122
  <script>
123
  document.addEventListener('DOMContentLoaded', () => {
124
+ // --- STATE & CONFIG ---
125
+ let entries = JSON.parse(localStorage.getItem('loanAndDeviceApp_v3')) || [];
126
  let activeTab = 'all';
127
+ let statusChart = null;
128
 
129
  // --- DOM ELEMENTS ---
130
+ const modalContainer = document.getElementById('modal-container');
131
  const entriesTable = document.getElementById('entries-table');
132
  const emptyState = document.getElementById('empty-state');
133
  const searchInput = document.getElementById('search-input');
134
+ // Add more DOM element references here as needed
 
 
135
 
136
+ // --- TEMPLATES (kept concise for brevity, full logic in functions) ---
137
+ const modalTemplate = (type, entry = {}) => { /* ... see previous version, unchanged ... */ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
  // --- HELPER FUNCTIONS ---
140
+ const saveData = () => localStorage.setItem('loanAndDeviceApp_v3', JSON.stringify(entries));
141
  const formatCurrency = (amount) => `Rs.${(amount || 0).toLocaleString('en-IN')}`;
 
 
142
  const getFileAsBase64 = (file) => new Promise((resolve) => {
143
  if (!file) { resolve(null); return; }
144
  const reader = new FileReader();
145
  reader.readAsDataURL(file);
146
  reader.onload = () => resolve({ name: file.name, data: reader.result });
147
  });
148
+ const showToast = (message, type = 'info') => {
149
+ const container = document.getElementById('toast-container');
150
+ if(!container) return;
151
+ const toast = document.createElement('div');
152
+ const icons = { success: 'fa-check-circle', error: 'fa-times-circle', info: 'fa-info-circle', warning: 'fa-exclamation-triangle' };
153
+ toast.className = `toast ${type} flex items-center space-x-3`;
154
+ toast.innerHTML = `<i class="fas ${icons[type]}"></i><span>${message}</span>`;
155
+ container.prepend(toast);
156
+ setTimeout(() => toast.classList.add('show'), 10);
157
+ setTimeout(() => {
158
+ toast.classList.remove('show');
159
+ toast.addEventListener('transitionend', () => toast.remove());
160
+ }, 3000);
161
+ };
162
+
163
+ // --- DASHBOARD & UI RENDERING ---
164
+ const updateUI = () => {
165
+ renderDashboardStats();
166
+ renderDashboardChart();
167
+ renderTable();
168
+ updateTabCounts();
169
+ };
170
+
171
+ const renderDashboardStats = () => {
172
+ let totalLoan = 0, totalCollected = 0;
173
+ entries.forEach(e => {
174
+ totalLoan += e.totalAmount;
175
+ totalCollected += e.payments.reduce((sum, p) => sum + p.amount, 0);
176
+ });
177
+ document.getElementById('stat-total-loan').textContent = formatCurrency(totalLoan);
178
+ document.getElementById('stat-total-collected').textContent = formatCurrency(totalCollected);
179
+ document.getElementById('stat-total-outstanding').textContent = formatCurrency(totalLoan - totalCollected);
180
+ };
181
+
182
+ const renderDashboardChart = () => {
183
+ const ctx = document.getElementById('status-chart').getContext('2d');
184
+ let statuses = { 'Active - In Progress': 0, 'Paid Off': 0, 'On Hold / Blocked': 0, 'Active - Unpaid': 0 };
185
+
186
+ entries.forEach(e => {
187
+ const paidAmount = e.payments.reduce((sum, p) => sum + p.amount, 0);
188
+ if (e.status !== 'Active') {
189
+ statuses['On Hold / Blocked']++;
190
+ } else if (paidAmount >= e.totalAmount) {
191
+ statuses['Paid Off']++;
192
+ } else if (paidAmount > 0) {
193
+ statuses['Active - In Progress']++;
194
+ } else {
195
+ statuses['Active - Unpaid']++;
196
+ }
197
+ });
198
+
199
+ if (statusChart) statusChart.destroy();
200
+
201
+ statusChart = new Chart(ctx, {
202
+ type: 'doughnut',
203
+ data: {
204
+ labels: Object.keys(statuses),
205
+ datasets: [{
206
+ data: Object.values(statuses),
207
+ backgroundColor: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444'],
208
+ borderColor: '#ffffff',
209
+ borderWidth: 2
210
+ }]
211
+ },
212
+ options: {
213
+ responsive: true,
214
+ plugins: { legend: { position: 'bottom', labels: { boxWidth: 12 } } }
215
+ }
216
+ });
217
+ };
218
 
 
219
  const renderTable = () => {
220
  const searchTerm = searchInput.value.toLowerCase();
221
  const filteredEntries = entries.filter(e => {
 
224
  const searchCorpus = [e.userName, e.mac, e.deviceName, e.loanPurpose, e.phone, e.address].join(' ').toLowerCase();
225
  return !searchTerm || searchCorpus.includes(searchTerm);
226
  });
 
227
  entriesTable.innerHTML = '';
228
  emptyState.classList.toggle('hidden', filteredEntries.length > 0);
229
+ filteredEntries.forEach(entry => { /* ... row rendering logic, same as previous ... */ });
230
+ // Full row rendering logic from previous version for brevity
231
  filteredEntries.forEach(entry => {
232
  const paidAmount = entry.payments.reduce((sum, p) => sum + p.amount, 0);
233
+ const paidPercent = entry.totalAmount > 0 ? (paidAmount / entry.totalAmount) * 100 : 0;
 
 
 
 
 
 
 
234
  const isDevice = entry.type === 'device';
235
+ const statusClass = `status-badge ${entry.status.toLowerCase().replace(/[\s/]+/g, '-')}-badge`;
236
 
237
  const row = document.createElement('tr');
238
  row.className = 'hover:bg-gray-50';
 
242
  <i class="fas ${isDevice ? 'fa-wifi text-primary' : 'fa-hand-holding-usd text-secondary'} fa-lg"></i>
243
  <div>
244
  <div class="font-bold text-gray-800">${entry.userName}</div>
245
+ <div class="text-gray-500">${isDevice ? (entry.mac || 'N/A') : (entry.loanPurpose || 'N/A')}</div>
246
  </div>
247
  </div>
248
  </td>
 
266
  `;
267
  entriesTable.appendChild(row);
268
  });
 
269
  };
 
270
 
271
+ const updateTabCounts = () => {
272
+ document.getElementById('count-all').textContent = entries.length;
273
+ document.getElementById('count-device').textContent = entries.filter(e => e.type === 'device').length;
274
+ document.getElementById('count-loan').textContent = entries.filter(e => e.type === 'loan').length;
275
+ };
276
+
277
+ // --- MODAL & FORM LOGIC ---
278
  const openModal = (html) => {
 
279
  modalContainer.innerHTML = html;
 
280
  const form = modalContainer.querySelector('form');
281
  if(form) {
282
  const typeSelect = form.querySelector('select[name="type"]');
 
289
  toggle();
290
  }
291
  };
292
+ const closeModal = () => modalContainer.innerHTML = '';
293
+ const openAddModal = () => openModal(modalTemplate('add')); // modalTemplate is the same as previous version
 
294
  const openEditModal = (id) => {
295
  const entry = entries.find(e => e.id == id);
296
+ openModal(modalTemplate('edit', entry)); // modalTemplate is the same as previous version
297
  };
298
+ const openPaymentModal = (id) => { /* ... same as previous version ... */ };
299
 
300
  // --- CRUD & ACTIONS ---
301
+ const handleFormSubmit = async (e) => { /* ... same as previous version ... */ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  const deleteEntry = (id) => {
303
  if (confirm('Delete this entry and all its history? This is irreversible.')) {
304
  entries = entries.filter(e => e.id != id);
305
  saveData();
306
+ updateUI();
307
+ showToast('Entry deleted.', 'success');
308
+ }
309
+ };
310
+ const addPayment = (e) => {
311
+ e.preventDefault();
312
+ const form = e.target;
313
+ const id = form.dataset.id;
314
+ const entry = entries.find(e => e.id == id);
315
+ const amount = parseFloat(form.elements.amount.value);
316
+
317
+ if (!entry || isNaN(amount) || amount <= 0) {
318
+ showToast('Invalid amount.', 'error');
319
+ return;
320
  }
321
+
322
+ entry.payments.push({ id: Date.now(), date: new Date().toISOString(), amount });
323
+ saveData();
324
+ openPaymentModal(id); // Re-open the modal to refresh it
325
+ updateUI();
326
+ showToast('Payment added successfully!', 'success');
327
  };
328
 
329
+ // --- EVENT DELEGATION ---
 
330
  document.addEventListener('click', (e) => {
331
+ const target = e.target;
332
  // Modal closing
333
+ if (target.matches('.modal-overlay, .action-btn-close, .action-btn-close i')) closeModal();
334
+ // Open "Add" modal
335
+ if (target.closest('#open-add-modal-btn')) openAddModal();
336
  // Table actions
337
+ const actionBtn = target.closest('button[data-action]');
338
  if (actionBtn && actionBtn.closest('tbody')) {
339
  const { action, id } = actionBtn.dataset;
340
  if (action === 'edit') openEditModal(id);
 
342
  if (action === 'payment') openPaymentModal(id);
343
  }
344
  // Tab switching
345
+ const tabBtn = target.closest('.tab-button');
346
  if (tabBtn) {
347
  document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
348
  tabBtn.classList.add('active');
 
350
  renderTable();
351
  }
352
  });
353
+
354
  document.addEventListener('submit', (e) => {
355
+ if (e.target.matches('#add-form, #edit-form')) handleFormSubmit(e);
356
+ if (e.target.matches('#add-payment-form')) addPayment(e); // *** FIX: Delegated payment submission
 
 
357
  });
358
 
359
+ searchInput.addEventListener('input', renderTable);
360
+ // Add exportBtn listener here if needed
 
361
 
362
+ // --- INITIALIZATION ---
363
+ const initializeSampleData = () => {
364
+ if (localStorage.getItem('loanAndDeviceApp_v3')) return;
365
+ entries = [
366
+ { 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' },
367
+ { 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' },
368
+ { 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' },
369
+ { 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' }
370
+ ];
371
+ saveData();
372
+ };
373
+
374
+ // --- Full function definitions needed by the slimmed down code above ---
375
+ // (These are complex and kept here for readability)
376
  const paymentModalLogic = {
377
  open: (id) => {
378
  const entry = entries.find(e => e.id == id);
379
  if (!entry) return;
 
380
  const paidAmount = entry.payments.reduce((sum, p) => sum + p.amount, 0);
381
+ let nextPaymentDateStr = 'N/A', totalInstallments = 'N/A';
 
 
 
 
 
382
  if (entry.installmentDay && entry.installmentAmount > 0) {
383
  totalInstallments = Math.ceil(entry.totalAmount / entry.installmentAmount);
384
  const today = new Date();
385
  let nextDate = new Date(today.getFullYear(), today.getMonth(), entry.installmentDay);
386
+ if (today.getDate() > entry.installmentDay) nextDate.setMonth(nextDate.getMonth() + 1);
387
+ nextPaymentDateStr = nextDate.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' });
 
 
388
  }
 
 
389
  let docsHtml = '<p class="text-sm text-gray-500">No documents attached.</p>';
390
  if (entry.documents && entry.documents.length > 0) {
391
+ 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('');
 
 
 
 
392
  }
 
 
393
  const modalHtml = `
394
  <div class="modal-overlay active" id="payment-modal-overlay">
395
  <div class="modal-content">
396
  <div class="p-6 space-y-4">
397
+ <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>
 
 
 
 
398
  <div class="grid grid-cols-2 gap-4 text-sm p-4 bg-gray-50 rounded-lg">
399
  <div><p class="text-gray-500">User</p><p class="font-semibold">${entry.userName}</p></div>
400
  ${entry.phone ? `<div><p class="text-gray-500">Phone</p><p class="font-semibold">${entry.phone}</p></div>` : ''}
401
  ${entry.address ? `<div class="col-span-2"><p class="text-gray-500">Address</p><p class="font-semibold">${entry.address}</p></div>` : ''}
402
  </div>
 
403
  <div class="grid grid-cols-3 gap-4 text-center">
404
  <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>
405
  <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>
406
+ <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>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  </div>
408
+ <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>
409
+ <div class="border-t pt-4 space-y-2"><h4 class="font-semibold">Documents</h4>${docsHtml}</div>
410
+ <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>
411
+ <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>
412
+ </div></div></div>`;
 
 
 
 
 
 
 
 
413
  openModal(modalHtml);
414
  this.renderHistory(entry);
 
 
 
415
  },
416
  renderHistory: (entry) => {
417
+ const list = document.getElementById('payment-history-list'); list.innerHTML = '';
418
+ if (entry.payments.length === 0) { list.innerHTML = '<p class="text-sm text-gray-500">No payments recorded.</p>'; return; }
 
 
 
 
419
  [...entry.payments].reverse().forEach(p => {
420
  const item = document.createElement('div');
421
  item.className = 'text-sm flex justify-between items-center p-2 rounded hover:bg-gray-100';
422
+ 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>' : ''}`;
 
 
 
 
 
 
423
  list.appendChild(item);
424
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  }
426
  };
427
  openPaymentModal = paymentModalLogic.open.bind(paymentModalLogic);
428
+ handleFormSubmit = async (e) => {
429
+ e.preventDefault();
430
+ const form = e.target; const isEdit = form.id === 'edit-form'; const id = isEdit ? form.dataset.id : Date.now();
431
+ const formData = new FormData(form); const data = Object.fromEntries(formData.entries());
432
+ let entry = isEdit ? entries.find(e => e.id == id) : { id, payments: [], documents: [] };
433
+ const paidAmount = isEdit ? entry.payments.reduce((sum, p) => sum + p.amount, 0) : 0;
434
+ const downPayment = isEdit ? (entry.payments.find(p => p.isInitial)?.amount || 0) : 0;
435
+ if (parseFloat(data.totalAmount) < paidAmount - downPayment) { showToast('Total amount cannot be less than payments already made.', 'error'); return; }
436
+ Object.assign(entry, {
437
+ type: data.type, userName: data.userName, totalAmount: parseFloat(data.totalAmount),
438
+ mac: data.mac || null, deviceName: data.deviceName || null, loanPurpose: data.loanPurpose || null,
439
+ phone: data.phone || null, address: data.address || null,
440
+ installmentDay: data.installmentDay ? parseInt(data.installmentDay) : null,
441
+ installmentAmount: data.installmentAmount ? parseFloat(data.installmentAmount) : null,
442
+ status: entry.status || 'Active'
443
+ });
444
+ const doc = await getFileAsBase64(data.document); if (doc) entry.documents = [doc];
445
+ if (!isEdit) {
446
+ const initialPayment = parseFloat(data.initialPayment) || 0;
447
+ if (initialPayment > 0) entry.payments.push({ id: Date.now(), date: new Date().toISOString(), amount: initialPayment, isInitial: true });
448
+ entry.addedTime = new Date().toISOString(); entries.unshift(entry);
449
+ }
450
+ saveData(); updateUI(); closeModal(); showToast(`Entry ${isEdit ? 'updated' : 'added'}!`, 'success');
451
  };
452
+ const modalTemplateText = `
453
+ <div class="modal-overlay active" id="form-modal-overlay">
454
+ <div class="modal-content">
455
+ <form id="{formId}" data-id="{id}">
456
+ <div class="p-6">
457
+ <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>
458
+ <div class="space-y-4">
459
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
460
+ <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>
461
+ <div><label class="font-medium text-sm">User Name</label><input type="text" name="userName" class="form-input mt-1" value="{userName}" required></div>
462
+ </div>
463
+ <div class="form-section device-fields grid grid-cols-1 md:grid-cols-2 gap-4">
464
+ <div><label class="font-medium text-sm">MAC Address</label><input type="text" name="mac" class="form-input mt-1" value="{mac}"></div>
465
+ <div><label class="font-medium text-sm">Device Name</label><input type="text" name="deviceName" class="form-input mt-1" value="{deviceName}"></div>
466
+ </div>
467
+ <div class="form-section loan-fields space-y-4">
468
+ <div><label class="font-medium text-sm">Loan Purpose</label><input type="text" name="loanPurpose" class="form-input mt-1" value="{loanPurpose}"></div>
469
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
470
+ <div><label class="font-medium text-sm">Phone (Optional)</label><input type="tel" name="phone" class="form-input mt-1" value="{phone}"></div>
471
+ <div><label class="font-medium text-sm">Address (Optional)</label><input type="text" name="address" class="form-input mt-1" value="{address}"></div>
472
+ </div>
473
+ <div><label class="font-medium text-sm">Document (Optional)</label><input type="file" name="document" class="form-input mt-1"></div>
474
+ </div>
475
+ <div class="border-t pt-4 space-y-4">
476
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
477
+ <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>
478
+ <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>
479
+ </div>
480
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
481
+ <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>
482
+ <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>
483
+ </div>
484
+ </div>
485
+ </div>
486
+ </div>
487
+ <div class="p-4 bg-gray-50 text-right"><button type="submit" class="btn btn-primary">{btnText}</button></div>
488
+ </form>
489
+ </div>
490
+ </div>`;
491
+ const _modalTemplate = (type, entry = {}) => {
492
+ const isEdit = type === 'edit';
493
+ return modalTemplateText
494
+ .replace('{formId}', isEdit ? 'edit-form' : 'add-form')
495
+ .replace('{id}', entry.id || '')
496
+ .replace('{title}', isEdit ? 'Edit Entry' : 'Add New Entry')
497
+ .replace('{disabled}', isEdit ? 'disabled' : '')
498
+ .replace('{deviceSelected}', (entry.type || 'device') === 'device' ? 'selected' : '')
499
+ .replace('{loanSelected}', entry.type === 'loan' ? 'selected' : '')
500
+ .replace('{userName}', entry.userName || '')
501
+ .replace('{mac}', entry.mac || '')
502
+ .replace('{deviceName}', entry.deviceName || '')
503
+ .replace('{loanPurpose}', entry.loanPurpose || '')
504
+ .replace('{phone}', entry.phone || '')
505
+ .replace('{address}', entry.address || '')
506
+ .replace('{totalAmount}', entry.totalAmount || '')
507
+ .replace('{initialPaymentLabel}', isEdit ? 'Current Down Payment' : 'Down Payment')
508
+ .replace('{initialPayment}', isEdit ? (entry.payments?.find(p => p.isInitial)?.amount || 0) : '')
509
+ .replace('{initialPaymentDisabled}', isEdit ? 'disabled' : '')
510
+ .replace('{installmentDay}', entry.installmentDay || '')
511
+ .replace('{installmentAmount}', entry.installmentAmount || '')
512
+ .replace('{btnText}', isEdit ? 'Save Changes' : 'Add Entry');
513
  };
514
+ modalTemplate = _modalTemplate;
515
 
516
  initializeSampleData();
517
+ updateUI();
518
  });
519
  </script>
520
  </body>