| | <!DOCTYPE html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>Offline Customer Management</title> |
| | <script src="https://cdn.tailwindcss.com"></script> |
| | <script src="https://cdn.sheetjs.com/xlsx-0.19.3/package/dist/xlsx.full.min.js"></script> |
| | <style> |
| | .photo-preview { |
| | width: 120px; |
| | height: 120px; |
| | object-fit: cover; |
| | border-radius: 50%; |
| | border: 3px solid #3b82f6; |
| | } |
| | #customerTable { |
| | font-size: 14px; |
| | } |
| | #customerTable th { |
| | background-color: #f3f4f6; |
| | position: sticky; |
| | top: 0; |
| | } |
| | .action-btn { |
| | padding: 2px 8px; |
| | font-size: 12px; |
| | } |
| | </style> |
| | </head> |
| | <body class="bg-gray-50 min-h-screen"> |
| | <div class="container mx-auto px-4 py-6"> |
| | <h1 class="text-2xl font-bold text-center text-blue-600 mb-6">Offline Customer Manager</h1> |
| | |
| | |
| | <div class="bg-white rounded-lg shadow-md p-5 mb-6"> |
| | <h2 class="text-lg font-semibold mb-3 text-gray-800 border-b pb-2"> |
| | <span id="formTitle">Add New Customer</span> |
| | <button id="clearForm" class="float-right text-sm text-blue-600 hover:text-blue-800">Clear Form</button> |
| | </h2> |
| | |
| | <form id="customerForm" class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| | <input type="hidden" id="customerId"> |
| | |
| | |
| | <div class="space-y-3"> |
| | <div> |
| | <label for="name" class="block text-sm font-medium text-gray-700">Full Name*</label> |
| | <input type="text" id="name" name="name" required |
| | class="mt-1 block w-full rounded border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 p-2 border text-sm"> |
| | </div> |
| | |
| | <div> |
| | <label for="email" class="block text-sm font-medium text-gray-700">Email*</label> |
| | <input type="email" id="email" name="email" required |
| | class="mt-1 block w-full rounded border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 p-2 border text-sm"> |
| | </div> |
| | |
| | <div> |
| | <label for="phone" class="block text-sm font-medium text-gray-700">Phone*</label> |
| | <input type="tel" id="phone" name="phone" required |
| | class="mt-1 block w-full rounded border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 p-2 border text-sm"> |
| | </div> |
| | </div> |
| | |
| | |
| | <div class="space-y-3"> |
| | <div> |
| | <label for="company" class="block text-sm font-medium text-gray-700">Company</label> |
| | <input type="text" id="company" name="company" |
| | class="mt-1 block w-full rounded border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 p-2 border text-sm"> |
| | </div> |
| | |
| | <div> |
| | <label for="status" class="block text-sm font-medium text-gray-700">Status</label> |
| | <select id="status" name="status" |
| | class="mt-1 block w-full rounded border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 p-2 border text-sm"> |
| | <option value="Active">Active</option> |
| | <option value="Inactive">Inactive</option> |
| | <option value="Prospect">Prospect</option> |
| | </select> |
| | </div> |
| | |
| | <div class="flex items-end gap-2"> |
| | <div class="flex-1"> |
| | <label for="lastContact" class="block text-sm font-medium text-gray-700">Last Contact</label> |
| | <input type="date" id="lastContact" name="lastContact" |
| | class="mt-1 block w-full rounded border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 p-2 border text-sm"> |
| | </div> |
| | <button type="button" id="setToday" class="text-sm bg-gray-200 hover:bg-gray-300 px-2 py-1 rounded">Today</button> |
| | </div> |
| | </div> |
| | |
| | |
| | <div class="md:col-span-2 space-y-3"> |
| | <div> |
| | <label for="notes" class="block text-sm font-medium text-gray-700">Notes</label> |
| | <textarea id="notes" name="notes" rows="2" |
| | class="mt-1 block w-full rounded border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 p-2 border text-sm"></textarea> |
| | </div> |
| | |
| | <div class="flex justify-between items-center"> |
| | <div> |
| | <button type="submit" id="saveBtn" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm"> |
| | Save Customer |
| | </button> |
| | <button type="button" id="cancelBtn" class="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 ml-2 text-sm hidden"> |
| | Cancel |
| | </button> |
| | </div> |
| | <button type="button" id="exportBtn" class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 text-sm"> |
| | Export to Excel |
| | </button> |
| | </div> |
| | </div> |
| | </form> |
| | </div> |
| | |
| | |
| | <div class="bg-white rounded-lg shadow-md p-4"> |
| | <div class="flex justify-between items-center mb-3"> |
| | <h2 class="text-lg font-semibold text-gray-800">Customer List</h2> |
| | <div class="flex items-center gap-2"> |
| | <input type="text" id="searchInput" placeholder="Search customers..." |
| | class="border rounded p-1 text-sm w-48"> |
| | <button id="clearSearch" class="text-sm text-gray-500 hover:text-gray-700">Clear</button> |
| | </div> |
| | </div> |
| | |
| | <div class="overflow-x-auto"> |
| | <table id="customerTable" class="w-full border-collapse"> |
| | <thead> |
| | <tr class="text-left text-sm"> |
| | <th class="p-2 border-b">Name</th> |
| | <th class="p-2 border-b">Email</th> |
| | <th class="p-2 border-b">Phone</th> |
| | <th class="p-2 border-b">Company</th> |
| | <th class="p-2 border-b">Status</th> |
| | <th class="p-2 border-b">Last Contact</th> |
| | <th class="p-2 border-b text-center">Actions</th> |
| | </tr> |
| | </thead> |
| | <tbody id="customerTableBody" class="text-sm"> |
| | |
| | </tbody> |
| | </table> |
| | </div> |
| | |
| | <div class="mt-3 flex justify-between items-center text-sm text-gray-600"> |
| | <div id="customerCount">0 customers</div> |
| | <button id="deleteAllBtn" class="text-red-600 hover:text-red-800">Delete All Customers</button> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <script> |
| | |
| | let customers = JSON.parse(localStorage.getItem('customers')) || []; |
| | let isEditing = false; |
| | let currentEditId = null; |
| | |
| | |
| | const customerForm = document.getElementById('customerForm'); |
| | const customerTableBody = document.getElementById('customerTableBody'); |
| | const customerCount = document.getElementById('customerCount'); |
| | const searchInput = document.getElementById('searchInput'); |
| | const clearSearch = document.getElementById('clearSearch'); |
| | const clearForm = document.getElementById('clearForm'); |
| | const saveBtn = document.getElementById('saveBtn'); |
| | const cancelBtn = document.getElementById('cancelBtn'); |
| | const exportBtn = document.getElementById('exportBtn'); |
| | const deleteAllBtn = document.getElementById('deleteAllBtn'); |
| | const setToday = document.getElementById('setToday'); |
| | |
| | |
| | setToday.addEventListener('click', () => { |
| | const today = new Date().toISOString().split('T')[0]; |
| | document.getElementById('lastContact').value = today; |
| | }); |
| | |
| | |
| | clearForm.addEventListener('click', (e) => { |
| | e.preventDefault(); |
| | resetForm(); |
| | }); |
| | |
| | |
| | clearSearch.addEventListener('click', () => { |
| | searchInput.value = ''; |
| | renderCustomerTable(customers); |
| | }); |
| | |
| | |
| | searchInput.addEventListener('input', (e) => { |
| | const searchTerm = e.target.value.toLowerCase(); |
| | const filteredCustomers = customers.filter(customer => |
| | customer.name.toLowerCase().includes(searchTerm) || |
| | customer.email.toLowerCase().includes(searchTerm) || |
| | customer.phone.toLowerCase().includes(searchTerm) || |
| | (customer.company && customer.company.toLowerCase().includes(searchTerm)) |
| | ); |
| | renderCustomerTable(filteredCustomers); |
| | }); |
| | |
| | |
| | customerForm.addEventListener('submit', (e) => { |
| | e.preventDefault(); |
| | |
| | const customerData = { |
| | id: isEditing ? currentEditId : Date.now().toString(), |
| | name: document.getElementById('name').value, |
| | email: document.getElementById('email').value, |
| | phone: document.getElementById('phone').value, |
| | company: document.getElementById('company').value, |
| | status: document.getElementById('status').value, |
| | lastContact: document.getElementById('lastContact').value, |
| | notes: document.getElementById('notes').value, |
| | createdAt: isEditing ? |
| | customers.find(c => c.id === currentEditId).createdAt : |
| | new Date().toISOString(), |
| | updatedAt: new Date().toISOString() |
| | }; |
| | |
| | if (isEditing) { |
| | |
| | customers = customers.map(customer => |
| | customer.id === currentEditId ? customerData : customer |
| | ); |
| | } else { |
| | |
| | customers.push(customerData); |
| | } |
| | |
| | |
| | localStorage.setItem('customers', JSON.stringify(customers)); |
| | |
| | |
| | renderCustomerTable(customers); |
| | resetForm(); |
| | |
| | |
| | exportToExcel(); |
| | }); |
| | |
| | |
| | cancelBtn.addEventListener('click', () => { |
| | resetForm(); |
| | }); |
| | |
| | |
| | exportBtn.addEventListener('click', () => { |
| | exportToExcel(); |
| | }); |
| | |
| | |
| | deleteAllBtn.addEventListener('click', () => { |
| | if (confirm('Are you sure you want to delete ALL customers? This cannot be undone.')) { |
| | customers = []; |
| | localStorage.setItem('customers', JSON.stringify(customers)); |
| | renderCustomerTable(customers); |
| | resetForm(); |
| | } |
| | }); |
| | |
| | |
| | function renderCustomerTable(customersToRender) { |
| | customerTableBody.innerHTML = ''; |
| | |
| | if (customersToRender.length === 0) { |
| | customerTableBody.innerHTML = ` |
| | <tr> |
| | <td colspan="7" class="p-4 text-center text-gray-500">No customers found</td> |
| | </tr> |
| | `; |
| | customerCount.textContent = '0 customers'; |
| | return; |
| | } |
| | |
| | customersToRender.forEach(customer => { |
| | const row = document.createElement('tr'); |
| | row.className = 'border-b hover:bg-gray-50'; |
| | row.innerHTML = ` |
| | <td class="p-2">${customer.name}</td> |
| | <td class="p-2">${customer.email}</td> |
| | <td class="p-2">${customer.phone}</td> |
| | <td class="p-2">${customer.company || '-'}</td> |
| | <td class="p-2"> |
| | <span class="px-2 py-1 rounded-full text-xs |
| | ${customer.status === 'Active' ? 'bg-green-100 text-green-800' : |
| | customer.status === 'Inactive' ? 'bg-red-100 text-red-800' : |
| | 'bg-yellow-100 text-yellow-800'}"> |
| | ${customer.status} |
| | </span> |
| | </td> |
| | <td class="p-2">${customer.lastContact || '-'}</td> |
| | <td class="p-2 text-center"> |
| | <button onclick="editCustomer('${customer.id}')" class="action-btn bg-blue-100 text-blue-800 hover:bg-blue-200"> |
| | Edit |
| | </button> |
| | <button onclick="deleteCustomer('${customer.id}')" class="action-btn bg-red-100 text-red-800 hover:bg-red-200 ml-1"> |
| | Delete |
| | </button> |
| | </td> |
| | `; |
| | customerTableBody.appendChild(row); |
| | }); |
| | |
| | customerCount.textContent = `${customersToRender.length} customer${customersToRender.length !== 1 ? 's' : ''}`; |
| | } |
| | |
| | |
| | window.editCustomer = function(id) { |
| | const customer = customers.find(c => c.id === id); |
| | if (!customer) return; |
| | |
| | isEditing = true; |
| | currentEditId = id; |
| | |
| | |
| | document.getElementById('formTitle').textContent = 'Edit Customer'; |
| | saveBtn.textContent = 'Update Customer'; |
| | cancelBtn.classList.remove('hidden'); |
| | |
| | |
| | document.getElementById('customerId').value = customer.id; |
| | document.getElementById('name').value = customer.name; |
| | document.getElementById('email').value = customer.email; |
| | document.getElementById('phone').value = customer.phone; |
| | document.getElementById('company').value = customer.company || ''; |
| | document.getElementById('status').value = customer.status; |
| | document.getElementById('lastContact').value = customer.lastContact || ''; |
| | document.getElementById('notes').value = customer.notes || ''; |
| | |
| | |
| | customerForm.scrollIntoView({ behavior: 'smooth' }); |
| | }; |
| | |
| | |
| | window.deleteCustomer = function(id) { |
| | if (!confirm('Are you sure you want to delete this customer?')) return; |
| | |
| | customers = customers.filter(customer => customer.id !== id); |
| | localStorage.setItem('customers', JSON.stringify(customers)); |
| | renderCustomerTable(customers); |
| | |
| | |
| | if (isEditing && currentEditId === id) { |
| | resetForm(); |
| | } |
| | |
| | |
| | exportToExcel(); |
| | }; |
| | |
| | |
| | function resetForm() { |
| | customerForm.reset(); |
| | document.getElementById('customerId').value = ''; |
| | isEditing = false; |
| | currentEditId = null; |
| | |
| | |
| | document.getElementById('formTitle').textContent = 'Add New Customer'; |
| | saveBtn.textContent = 'Save Customer'; |
| | cancelBtn.classList.add('hidden'); |
| | } |
| | |
| | |
| | function exportToExcel() { |
| | if (customers.length === 0) { |
| | alert('No customers to export'); |
| | return; |
| | } |
| | |
| | |
| | const exportData = customers.map(customer => ({ |
| | 'Name': customer.name, |
| | 'Email': customer.email, |
| | 'Phone': customer.phone, |
| | 'Company': customer.company || '', |
| | 'Status': customer.status, |
| | 'Last Contact': customer.lastContact || '', |
| | 'Notes': customer.notes || '', |
| | 'Created At': customer.createdAt, |
| | 'Updated At': customer.updatedAt |
| | })); |
| | |
| | |
| | const ws = XLSX.utils.json_to_sheet(exportData); |
| | |
| | |
| | const wb = XLSX.utils.book_new(); |
| | XLSX.utils.book_append_sheet(wb, ws, "Customers"); |
| | |
| | |
| | const today = new Date(); |
| | const dateStr = today.toISOString().split('T')[0]; |
| | const fileName = `Customers_${dateStr}.xlsx`; |
| | |
| | |
| | XLSX.writeFile(wb, fileName); |
| | } |
| | |
| | |
| | document.addEventListener('DOMContentLoaded', () => { |
| | renderCustomerTable(customers); |
| | |
| | |
| | const today = new Date().toISOString().split('T')[0]; |
| | document.getElementById('lastContact').value = today; |
| | }); |
| | </script> |
| | <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Seph0426/jowbox" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| | </html> |