600 lines
36 KiB
HTML
600 lines
36 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Customers (Rolodex) - Delphi Database{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-6">
|
|
<!-- Page Header -->
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center justify-center w-10 h-10 bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 rounded-xl">
|
|
<i class="fa-solid fa-address-book text-lg"></i>
|
|
</div>
|
|
<h1 class="text-2xl font-bold text-neutral-900 dark:text-neutral-100">Customers (Rolodex)</h1>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<button id="addCustomerBtn" class="flex items-center gap-2 px-4 py-2 bg-success-600 text-white hover:bg-success-700 rounded-lg transition-colors duration-200">
|
|
<i class="fa-solid fa-circle-plus"></i>
|
|
<span>New Customer</span>
|
|
<kbd class="hidden sm:inline-block ml-2 px-1.5 py-0.5 bg-success-700 rounded text-xs">Ctrl+N</kbd>
|
|
</button>
|
|
<button id="statsBtn" class="flex items-center gap-2 px-4 py-2 bg-info-600 text-white hover:bg-info-700 rounded-lg transition-colors duration-200">
|
|
<i class="fa-solid fa-chart-line"></i>
|
|
<span>Statistics</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search and Filter Panel -->
|
|
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft">
|
|
<div class="p-6">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div class="lg:col-span-2">
|
|
<label for="searchInput" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Search Customers</label>
|
|
<div class="relative">
|
|
<input type="text" id="searchInput" class="w-full pl-10 pr-4 py-3 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 placeholder-neutral-400 dark:placeholder-neutral-500 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" placeholder="Start typing to search customers...">
|
|
<div class="absolute inset-y-0 left-0 flex items-center pl-3">
|
|
<i class="fa-solid fa-magnifying-glass text-neutral-400 dark:text-neutral-500"></i>
|
|
</div>
|
|
<button id="searchBtn" class="absolute inset-y-0 right-0 flex items-center pr-3 text-neutral-400 hover:text-primary-600 dark:text-neutral-500 dark:hover:text-primary-400 transition-colors">
|
|
<i id="searchIcon" class="fa-solid fa-magnifying-glass text-lg"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label for="groupFilter" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Group Filter</label>
|
|
<select id="groupFilter" class="w-full px-3 py-3 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
<option value="">All Groups</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="stateFilter" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">State Filter</label>
|
|
<select id="stateFilter" class="w-full px-3 py-3 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
<option value="">All States</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-700">
|
|
<div class="max-w-md">
|
|
<label for="phoneSearch" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Phone Search</label>
|
|
<div class="relative">
|
|
<input type="text" id="phoneSearch" class="w-full pl-10 pr-4 py-3 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 placeholder-neutral-400 dark:placeholder-neutral-500 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200" placeholder="Type phone number to search...">
|
|
<div class="absolute inset-y-0 left-0 flex items-center pl-3">
|
|
<i class="fa-solid fa-phone text-neutral-400 dark:text-neutral-500"></i>
|
|
</div>
|
|
<button id="phoneSearchBtn" class="absolute inset-y-0 right-0 flex items-center pr-3 text-neutral-400 hover:text-primary-600 dark:text-neutral-500 dark:hover:text-primary-400 transition-colors">
|
|
<i class="fa-solid fa-arrow-right text-lg"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Customer List -->
|
|
<div class="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-soft overflow-hidden">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm text-left text-neutral-900 dark:text-neutral-100" id="customersTable">
|
|
<thead class="bg-neutral-50 dark:bg-neutral-800/50">
|
|
<tr class="border-b border-neutral-200 dark:border-neutral-700">
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">Customer</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">Name</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">Group</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">Location</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">Phone</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">Email</th>
|
|
<th class="px-4 py-3 text-right text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="customersTableBody" class="divide-y divide-neutral-200 dark:divide-neutral-700">
|
|
<!-- Customer rows will be populated here -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div id="emptyState" class="hidden flex flex-col items-center justify-center py-12 text-center">
|
|
<div class="w-16 h-16 bg-neutral-100 dark:bg-neutral-800 rounded-full flex items-center justify-center mb-4">
|
|
<i class="fa-solid fa-users text-2xl text-neutral-400 dark:text-neutral-500"></i>
|
|
</div>
|
|
<h3 class="text-lg font-medium text-neutral-900 dark:text-neutral-100 mb-2">No customers found</h3>
|
|
<p class="text-neutral-500 dark:text-neutral-400 mb-4">Get started by adding your first customer.</p>
|
|
<button onclick="showAddCustomerModal()" class="flex items-center gap-2 px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg transition-colors duration-200">
|
|
<i class="fa-solid fa-circle-plus"></i>
|
|
<span>Add Customer</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div class="px-6 py-4 border-t border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800/50">
|
|
<nav aria-label="Customer pagination">
|
|
<div id="pagination" class="flex items-center justify-center space-x-1">
|
|
<!-- Pagination will be populated here -->
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Customer Modal -->
|
|
<div id="customerModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-6xl w-full max-h-screen overflow-hidden">
|
|
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
|
<h2 id="customerModalLabel" class="text-xl font-semibold text-neutral-900 dark:text-neutral-100">Customer Details</h2>
|
|
<button onclick="closeCustomerModal()" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300 transition-colors">
|
|
<i class="fa-solid fa-xmark text-xl"></i>
|
|
</button>
|
|
</div>
|
|
<div class="px-6 py-4 max-h-96 overflow-y-auto scrollbar-thin">
|
|
<form id="customerForm">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
<!-- Personal Information -->
|
|
<div class="space-y-4">
|
|
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">Personal Information</h3>
|
|
<div>
|
|
<label for="customerId" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Customer ID <span class="text-danger-600">*</span></label>
|
|
<input type="text" id="customerId" required class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
</div>
|
|
<div>
|
|
<label for="last" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Last Name/Company <span class="text-danger-600">*</span></label>
|
|
<input type="text" id="last" required class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label for="first" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">First Name</label>
|
|
<input type="text" id="first" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
</div>
|
|
<div>
|
|
<label for="middle" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Middle Name</label>
|
|
<input type="text" id="middle" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<label for="prefix" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Prefix</label>
|
|
<input type="text" id="prefix" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
</div>
|
|
<div>
|
|
<label for="suffix" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Suffix</label>
|
|
<input type="text" id="suffix" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
</div>
|
|
<div>
|
|
<label for="title" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Title</label>
|
|
<input type="text" id="title" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label for="group" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Group</label>
|
|
<input type="text" id="group" list="groupList" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
<datalist id="groupList"></datalist>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Address Information -->
|
|
<div class="space-y-4">
|
|
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">Address</h3>
|
|
<div>
|
|
<label for="a1" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Address Line 1 / Firm</label>
|
|
<input type="text" id="a1" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
</div>
|
|
<div>
|
|
<label for="a2" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Address Line 2</label>
|
|
<input type="text" id="a2" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
</div>
|
|
<div>
|
|
<label for="a3" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Address Line 3</label>
|
|
<input type="text" id="a3" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
</div>
|
|
<div class="grid grid-cols-3 gap-4">
|
|
<div class="col-span-2">
|
|
<label for="city" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">City</label>
|
|
<input type="text" id="city" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
</div>
|
|
<div>
|
|
<label for="abrev" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">State</label>
|
|
<input type="text" id="abrev" list="stateList" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
<datalist id="stateList"></datalist>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label for="zip" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Zip Code</label>
|
|
<input type="text" id="zip" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contact Information -->
|
|
<div class="mb-6">
|
|
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">Contact Information</h3>
|
|
<div>
|
|
<label for="email" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Email</label>
|
|
<input type="email" id="email" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
</div>
|
|
<div id="phoneList" class="space-y-4 mt-4"></div>
|
|
<button type="button" id="addPhoneBtn" class="mt-2 flex items-center gap-2 px-3 py-2 bg-success-600 text-white hover:bg-success-700 rounded-lg transition-colors duration-200 text-sm">
|
|
<i class="fa-solid fa-circle-plus"></i>
|
|
Add Phone
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Additional Information -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
<div class="space-y-4">
|
|
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">Additional Details</h3>
|
|
<div>
|
|
<label for="dob" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Date of Birth</label>
|
|
<input type="date" id="dob" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
</div>
|
|
<div>
|
|
<label for="ss_number" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Social Security Number</label>
|
|
<input type="text" id="ss_number" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
</div>
|
|
<div>
|
|
<label for="legal_status" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Legal Status</label>
|
|
<input type="text" id="legal_status" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200">
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label for="memo" class="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Notes</label>
|
|
<textarea id="memo" rows="6" class="w-full px-3 py-2 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-600 rounded-lg text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200"></textarea>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800/50">
|
|
<button onclick="closeCustomerModal()" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg transition-colors duration-200">
|
|
Cancel
|
|
</button>
|
|
<button id="deleteCustomerBtn" class="hidden px-4 py-2 bg-danger-600 text-white hover:bg-danger-700 rounded-lg transition-colors duration-200">
|
|
<i class="fa-solid fa-trash"></i>
|
|
<span>Delete</span>
|
|
</button>
|
|
<button id="saveCustomerBtn" class="px-4 py-2 bg-primary-600 text-white hover:bg-primary-700 rounded-lg transition-colors duration-200">
|
|
<i class="fa-regular fa-circle-check"></i>
|
|
<span>Save</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Modal -->
|
|
<div id="statsModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
<div class="bg-white dark:bg-neutral-800 rounded-xl shadow-xl max-w-2xl w-full">
|
|
<div class="flex items-center justify-between px-6 py-4 border-b border-neutral-200 dark:border-neutral-700">
|
|
<h2 class="text-xl font-semibold text-neutral-900 dark:text-neutral-100">Customer Database Statistics</h2>
|
|
<button onclick="closeStatsModal()" class="text-neutral-400 hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300 transition-colors">
|
|
<i class="fa-solid fa-xmark text-xl"></i>
|
|
</button>
|
|
</div>
|
|
<div id="statsContent" class="px-6 py-4">
|
|
<!-- Statistics will be loaded here -->
|
|
</div>
|
|
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800/50">
|
|
<button onclick="closeStatsModal()" class="px-4 py-2 bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600 rounded-lg transition-colors duration-200">
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/static/js/customers-tailwind.js?v=12"></script>
|
|
<script src="/static/js/customers-modern.js?v=1"></script>
|
|
|
|
<script>
|
|
// Initialize on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Check authentication first
|
|
const token = localStorage.getItem('auth_token');
|
|
if (!token) {
|
|
window.location.href = '/login';
|
|
return;
|
|
}
|
|
|
|
loadCustomers();
|
|
loadGroups();
|
|
loadStates();
|
|
setupEventListeners();
|
|
});
|
|
|
|
function setupEventListeners() {
|
|
// Real-time search functionality with debouncing
|
|
let searchTimeout;
|
|
const searchInput = document.getElementById('searchInput');
|
|
|
|
searchInput.addEventListener('input', function(e) {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
performSearch();
|
|
}, 300); // Wait 300ms after user stops typing
|
|
});
|
|
|
|
// Keep existing functionality for search button and Enter key
|
|
document.getElementById('searchBtn').addEventListener('click', performSearch);
|
|
searchInput.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') {
|
|
clearTimeout(searchTimeout);
|
|
performSearch();
|
|
}
|
|
});
|
|
|
|
// Real-time phone search functionality with debouncing
|
|
let phoneSearchTimeout;
|
|
const phoneSearchInput = document.getElementById('phoneSearch');
|
|
|
|
phoneSearchInput.addEventListener('input', function(e) {
|
|
clearTimeout(phoneSearchTimeout);
|
|
phoneSearchTimeout = setTimeout(() => {
|
|
if (phoneSearchInput.value.trim()) {
|
|
performPhoneSearch();
|
|
} else {
|
|
// If phone search is cleared, go back to regular customer list
|
|
loadCustomers();
|
|
}
|
|
}, 300); // Wait 300ms after user stops typing
|
|
});
|
|
|
|
// Keep existing functionality for phone search button and Enter key
|
|
document.getElementById('phoneSearchBtn').addEventListener('click', performPhoneSearch);
|
|
phoneSearchInput.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') {
|
|
clearTimeout(phoneSearchTimeout);
|
|
performPhoneSearch();
|
|
}
|
|
});
|
|
|
|
// Modal buttons
|
|
document.getElementById('addCustomerBtn').addEventListener('click', showAddCustomerModal);
|
|
document.getElementById('saveCustomerBtn').addEventListener('click', saveCustomer);
|
|
document.getElementById('deleteCustomerBtn').addEventListener('click', deleteCustomer);
|
|
document.getElementById('statsBtn').addEventListener('click', showStats);
|
|
document.getElementById('addPhoneBtn').addEventListener('click', addPhoneNumber);
|
|
|
|
// Form validation
|
|
const customerIdInput = document.getElementById('customerId');
|
|
if (customerIdInput) {
|
|
customerIdInput.addEventListener('blur', validateCustomerId);
|
|
}
|
|
}
|
|
|
|
// Modal functions
|
|
function closeStatsModal() {
|
|
document.getElementById('statsModal').classList.add('hidden');
|
|
}
|
|
|
|
// Load customers with enhanced formatting
|
|
async function loadCustomers(page = 0, search = '') {
|
|
try {
|
|
// Show loading state
|
|
setSearchLoading(true);
|
|
|
|
const params = new URLSearchParams({
|
|
skip: page * 50,
|
|
limit: 50
|
|
});
|
|
|
|
if (search) params.append('search', search);
|
|
|
|
const response = await window.http.wrappedFetch(`/api/customers/?${params}`);
|
|
|
|
if (!response.ok) throw new Error('Failed to load customers');
|
|
|
|
const customers = await response.json();
|
|
displayCustomers(customers);
|
|
|
|
} catch (error) {
|
|
console.error('Error loading customers:', error);
|
|
showAlert('Error loading customers: ' + error.message, 'danger');
|
|
} finally {
|
|
// Hide loading state
|
|
setSearchLoading(false);
|
|
}
|
|
}
|
|
|
|
function setSearchLoading(isLoading) {
|
|
const searchIcon = document.getElementById('searchIcon');
|
|
if (isLoading) {
|
|
searchIcon.className = 'fa-solid fa-spinner fa-spin text-lg';
|
|
} else {
|
|
searchIcon.className = 'fa-solid fa-magnifying-glass text-lg';
|
|
}
|
|
}
|
|
|
|
function performSearch() {
|
|
currentSearch = document.getElementById('searchInput').value.trim();
|
|
currentPage = 0;
|
|
loadCustomers(currentPage, currentSearch);
|
|
}
|
|
|
|
async function performPhoneSearch() {
|
|
const phone = document.getElementById('phoneSearch').value;
|
|
if (!phone) return;
|
|
|
|
try {
|
|
const response = await window.http.wrappedFetch(`/api/customers/search/phone?phone=${encodeURIComponent(phone)}`);
|
|
|
|
if (!response.ok) throw new Error('Phone search failed');
|
|
|
|
const results = await response.json();
|
|
displayPhoneSearchResults(results);
|
|
|
|
} catch (error) {
|
|
console.error('Phone search error:', error);
|
|
showAlert('Phone search failed: ' + error.message, 'danger');
|
|
}
|
|
}
|
|
|
|
function displayPhoneSearchResults(results) {
|
|
const tbody = document.getElementById('customersTableBody');
|
|
const emptyState = document.getElementById('emptyState');
|
|
|
|
tbody.innerHTML = '';
|
|
|
|
if (!results || results.length === 0) {
|
|
emptyState.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
emptyState.classList.add('hidden');
|
|
|
|
results.forEach(result => {
|
|
const row = document.createElement('tr');
|
|
row.className = 'group border-b border-neutral-100 dark:border-neutral-700/50 hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/30 dark:hover:from-blue-900/10 dark:hover:to-indigo-900/10 transition-all duration-200 cursor-pointer';
|
|
|
|
// Store customer ID as data attribute to avoid escaping issues
|
|
row.dataset.customerId = result.customer.id;
|
|
|
|
row.innerHTML = `
|
|
<td class=\"px-4 py-4 customer-cell\">
|
|
<div class=\"text-sm font-mono font-semibold text-neutral-900 dark:text-neutral-100 bg-gradient-to-br from-neutral-50 to-neutral-100 dark:from-neutral-800 dark:to-neutral-700 px-3 py-2 rounded-lg shadow-sm border border-neutral-200/50 dark:border-neutral-600/50 group-hover:shadow-md transition-shadow\" title=\"${result.customer.id}\">${result.customer.id}</div>
|
|
</td>
|
|
<td class=\"px-4 py-4 customer-cell\">
|
|
<div class=\"text-sm font-semibold text-neutral-900 dark:text-neutral-100 group-hover:text-blue-700 dark:group-hover:text-blue-300 transition-colors\" title=\"${result.customer.name}\">${result.customer.name}</div>
|
|
</td>
|
|
<td class=\"px-4 py-4 customer-cell\">
|
|
<span class="text-neutral-400 dark:text-neutral-500 text-sm font-medium">-</span>
|
|
</td>
|
|
<td class=\"px-4 py-4 customer-cell\">
|
|
<div class=\"text-sm font-medium text-neutral-900 dark:text-neutral-100\" title=\"${result.customer.city}, ${result.customer.state}\">
|
|
${result.customer.city}, ${result.customer.state}
|
|
</div>
|
|
</td>
|
|
<td class=\"px-4 py-4 customer-cell\">
|
|
<div class=\"text-sm font-mono font-medium text-neutral-900 dark:text-neutral-100\" title=\"${result.location}: ${result.phone}\">
|
|
<div class="font-semibold text-warning-600 dark:text-warning-400">${result.location}: ${result.phone}</div>
|
|
</div>
|
|
</td>
|
|
<td class=\"px-4 py-4 customer-cell\">
|
|
<span class="text-neutral-400 dark:text-neutral-500 text-sm font-medium">-</span>
|
|
</td>
|
|
<td class=\"px-4 py-4 text-right\">
|
|
<div class=\"flex items-center justify-end space-x-2 opacity-70 group-hover:opacity-100 transition-opacity\">
|
|
<button class=\"view-phone-result-btn inline-flex items-center px-3 py-2 bg-gradient-to-r from-slate-100 to-slate-200 dark:from-slate-700 dark:to-slate-600 text-slate-700 dark:text-slate-200 hover:from-slate-200 hover:to-slate-300 dark:hover:from-slate-600 dark:hover:to-slate-500 rounded-lg text-sm font-semibold transition-all duration-200 shadow-sm hover:shadow-md border border-slate-300/50 dark:border-slate-500/50\">
|
|
<i class=\"fa-solid fa-eye mr-2\"></i>
|
|
View
|
|
</button>
|
|
<button class=\"edit-phone-result-btn inline-flex items-center px-3 py-2 bg-gradient-to-r from-blue-600 to-blue-700 dark:from-blue-700 dark:to-blue-800 text-white hover:from-blue-700 hover:to-blue-800 dark:hover:from-blue-600 dark:hover:to-blue-700 rounded-lg text-sm font-semibold transition-all duration-200 shadow-sm hover:shadow-md\">
|
|
<i class=\"fa-solid fa-pencil mr-2\"></i>
|
|
Edit
|
|
</button>
|
|
</div>
|
|
</td>
|
|
`;
|
|
|
|
// Add event listeners for clickable row (same as main customer table)
|
|
const customerCells = row.querySelectorAll('.customer-cell');
|
|
customerCells.forEach(cell => {
|
|
cell.addEventListener('click', () => viewCustomer(result.customer.id));
|
|
});
|
|
|
|
// Add event listener for the view button
|
|
const viewBtn = row.querySelector('.view-phone-result-btn');
|
|
viewBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
viewCustomer(result.customer.id);
|
|
});
|
|
|
|
// Add event listener for the edit button to avoid backslash escaping issues
|
|
const editBtn = row.querySelector('.edit-phone-result-btn');
|
|
editBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
editCustomer(result.customer.id);
|
|
});
|
|
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
async function loadGroups() {
|
|
try {
|
|
const response = await window.http.wrappedFetch('/api/customers/groups');
|
|
|
|
if (response.ok) {
|
|
const groups = await response.json();
|
|
const select = document.getElementById('groupFilter');
|
|
groups.forEach(g => {
|
|
const option = document.createElement('option');
|
|
option.value = g.group;
|
|
option.textContent = g.group;
|
|
select.appendChild(option);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading groups:', error);
|
|
}
|
|
}
|
|
|
|
async function loadStates() {
|
|
try {
|
|
const response = await window.http.wrappedFetch('/api/customers/states');
|
|
|
|
if (response.ok) {
|
|
const states = await response.json();
|
|
const select = document.getElementById('stateFilter');
|
|
states.forEach(s => {
|
|
const option = document.createElement('option');
|
|
option.value = s.state;
|
|
option.textContent = s.state;
|
|
select.appendChild(option);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading states:', error);
|
|
}
|
|
}
|
|
|
|
async function showStats() {
|
|
try {
|
|
const response = await window.http.wrappedFetch('/api/customers/stats');
|
|
|
|
if (!response.ok) throw new Error('Failed to load statistics');
|
|
|
|
const stats = await response.json();
|
|
displayStats(stats);
|
|
|
|
} catch (error) {
|
|
console.error('Error loading stats:', error);
|
|
showAlert('Error loading statistics: ' + error.message, 'danger');
|
|
}
|
|
}
|
|
|
|
function displayStats(stats) {
|
|
const content = document.getElementById('statsContent');
|
|
content.innerHTML = `
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-3">Database Overview</h3>
|
|
<div class="space-y-2">
|
|
<div class="flex justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Total Customers:</span>
|
|
<span class="font-semibold text-neutral-900 dark:text-neutral-100">${stats.total_customers}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">Phone Numbers:</span>
|
|
<span class="font-semibold text-neutral-900 dark:text-neutral-100">${stats.total_phone_numbers}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">With Email:</span>
|
|
<span class="font-semibold text-neutral-900 dark:text-neutral-100">${stats.customers_with_email}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-3">Group Breakdown</h3>
|
|
<div class="space-y-2">
|
|
${stats.group_breakdown.map(g => `
|
|
<div class="flex justify-between">
|
|
<span class="text-neutral-600 dark:text-neutral-400">${g.group}:</span>
|
|
<span class="font-semibold text-neutral-900 dark:text-neutral-100">${g.count}</span>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('statsModal').classList.remove('hidden');
|
|
}
|
|
|
|
// Functions are now implemented in the external customers-tailwind.js file
|
|
</script>
|
|
{% endblock %} |