Spaces:
Running
Running
| {% extends "base.html" %} | |
| {% block title %}Manage Client Executives — Admin{% endblock %} | |
| {% block content %} | |
| <div class="space-y-4"> | |
| <a href="/admin/?token={{ token }}" class="text-sm text-gray-400 hover:text-gray-600">← Admin Panel</a> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <h1 class="text-2xl font-bold sw-blue-text">Manage Client Executives</h1> | |
| <p class="text-gray-500 text-sm mt-1"> | |
| The CE list shown on the intake form. Separate from producers. Add, edit, or | |
| deactivate CEs — changes persist in the database (survive redeploys). | |
| </p> | |
| </div> | |
| {% if user.role == 'superadmin' %} | |
| <button onclick="openAddModal()" | |
| class="bg-green-600 text-white px-4 py-2 rounded-md text-sm font-semibold hover:bg-green-700"> | |
| + New Client Executive | |
| </button> | |
| {% endif %} | |
| </div> | |
| <!-- Filters --> | |
| <div class="bg-white rounded-lg shadow p-4 flex items-center justify-between gap-4"> | |
| <input type="text" id="searchBox" placeholder="Search by name or code…" | |
| oninput="filterRows()" | |
| class="flex-1 border border-gray-200 rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400"> | |
| <label class="flex items-center text-sm text-gray-600 cursor-pointer"> | |
| <input type="checkbox" id="show_inactive" onchange="toggleInactive()" | |
| {% if show_inactive %}checked{% endif %} class="mr-2"> | |
| Show inactive | |
| </label> | |
| </div> | |
| <!-- CE table --> | |
| <div class="bg-white rounded-lg shadow overflow-hidden"> | |
| <table class="w-full text-sm"> | |
| <thead> | |
| <tr class="sw-blue text-white text-left"> | |
| <th class="px-4 py-3 w-[5%]">#</th> | |
| <th class="px-4 py-3 w-[14%]">Code</th> | |
| <th class="px-4 py-3 w-[24%]">Name</th> | |
| <th class="px-4 py-3 w-[28%]">Email</th> | |
| <th class="px-4 py-3 w-[10%] text-center">Status</th> | |
| <th class="px-4 py-3 w-[19%] text-center">Action</th> | |
| </tr> | |
| </thead> | |
| <tbody id="ceTable"> | |
| {% for ce in ces %} | |
| <tr class="border-b hover:bg-gray-50 ce-row {% if not ce.is_active %}opacity-60{% endif %}" | |
| data-id="{{ ce.id }}" | |
| data-search="{{ ce.name|lower }} {{ ce.code|lower }}"> | |
| <td class="px-4 py-3 text-gray-400">{{ loop.index }}</td> | |
| <td class="px-4 py-3 font-mono"> | |
| <input type="text" id="code_{{ ce.id }}" value="{{ ce.code }}" | |
| class="w-24 border border-gray-200 rounded px-2 py-1 text-xs font-mono uppercase"> | |
| </td> | |
| <td class="px-4 py-3"> | |
| <input type="text" id="name_{{ ce.id }}" value="{{ ce.name }}" | |
| class="w-full border border-gray-200 rounded px-2 py-1 text-sm"> | |
| </td> | |
| <td class="px-4 py-3"> | |
| <input type="email" id="email_{{ ce.id }}" value="{{ ce.email or '' }}" | |
| placeholder="name@snellingswalters.com" | |
| class="w-full border border-gray-200 rounded px-2 py-1 text-sm {% if ce.email %}bg-green-50{% endif %}"> | |
| </td> | |
| <td class="px-4 py-3 text-center"> | |
| <span class="px-2 py-0.5 rounded text-xs font-semibold {% if ce.is_active %}bg-green-100 text-green-700{% else %}bg-red-100 text-red-700{% endif %}"> | |
| {{ 'Active' if ce.is_active else 'Inactive' }} | |
| </span> | |
| </td> | |
| <td class="px-4 py-3 text-center space-x-2"> | |
| {% if user.role == 'superadmin' %} | |
| <button onclick="saveRow({{ ce.id }})" | |
| class="bg-blue-600 text-white px-3 py-1 rounded text-xs font-semibold hover:bg-blue-700"> | |
| Save | |
| </button> | |
| <button onclick="toggleCE({{ ce.id }})" | |
| class="text-amber-600 hover:underline text-xs"> | |
| {{ 'Deactivate' if ce.is_active else 'Activate' }} | |
| </button> | |
| {% if not ce.is_active %} | |
| <button onclick="deleteCE({{ ce.id }}, '{{ ce.name }}')" | |
| class="text-red-600 hover:underline text-xs"> | |
| Delete | |
| </button> | |
| {% endif %} | |
| {% else %} | |
| <span class="text-gray-300 text-xs">View only</span> | |
| {% endif %} | |
| </td> | |
| </tr> | |
| {% endfor %} | |
| {% if not ces %} | |
| <tr><td colspan="6" class="px-4 py-6 text-center text-gray-400"> | |
| No client executives yet. Add one above, or paste a list below. | |
| </td></tr> | |
| {% endif %} | |
| </tbody> | |
| </table> | |
| </div> | |
| {% if user.role == 'superadmin' %} | |
| <!-- Bulk paste --> | |
| <div class="bg-white rounded-lg shadow p-5"> | |
| <h3 class="font-semibold sw-blue-text mb-2">Bulk Add / Update</h3> | |
| <p class="text-gray-400 text-xs mb-3">Paste one CE per line: <code>Full Name, email@example.com</code> (email optional). Codes auto-generate. Existing names get their email updated.</p> | |
| <textarea id="bulkInput" rows="5" placeholder="Jane Doe, jdoe@snellingswalters.com John Smith, jsmith@snellingswalters.com" | |
| class="w-full border border-gray-200 rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400"></textarea> | |
| <button onclick="bulkImport()" | |
| class="mt-2 bg-green-600 text-white px-6 py-2 rounded-lg text-sm font-semibold hover:bg-green-700 transition"> | |
| Import All | |
| </button> | |
| <span id="bulkResult" class="ml-3 text-sm text-gray-500"></span> | |
| </div> | |
| <!-- Add CE Modal --> | |
| <div id="addModal" class="fixed inset-0 bg-black/50 hidden flex items-center justify-center z-50"> | |
| <div class="bg-white rounded-lg shadow-xl w-full max-w-md p-6"> | |
| <h3 class="text-lg font-bold sw-blue-text mb-4">New Client Executive</h3> | |
| <div class="space-y-3"> | |
| <div> | |
| <label class="text-xs text-gray-500">Full Name</label> | |
| <input id="new_name" type="text" class="w-full border rounded-md px-3 py-2 text-sm" | |
| placeholder="Jane Doe"> | |
| </div> | |
| <div> | |
| <label class="text-xs text-gray-500">Code (optional — auto-generated if blank)</label> | |
| <input id="new_code" type="text" class="w-full border rounded-md px-3 py-2 text-sm font-mono uppercase" | |
| placeholder="DOEJA1"> | |
| </div> | |
| <div> | |
| <label class="text-xs text-gray-500">Email</label> | |
| <input id="new_email" type="email" class="w-full border rounded-md px-3 py-2 text-sm" | |
| placeholder="jdoe@snellingswalters.com"> | |
| </div> | |
| </div> | |
| <div class="flex justify-end space-x-3 mt-5"> | |
| <button onclick="closeAddModal()" class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800">Cancel</button> | |
| <button onclick="createCE()" class="bg-green-600 text-white px-4 py-2 rounded-md text-sm font-semibold hover:bg-green-700">Create</button> | |
| </div> | |
| </div> | |
| </div> | |
| {% endif %} | |
| </div> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| const authToken = '{{ token }}'; | |
| const authHeaders = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken}; | |
| function filterRows() { | |
| const q = document.getElementById('searchBox').value.toLowerCase(); | |
| document.querySelectorAll('.ce-row').forEach(r => { | |
| r.style.display = r.dataset.search.includes(q) ? '' : 'none'; | |
| }); | |
| } | |
| function toggleInactive() { | |
| const url = new URL(window.location); | |
| if (document.getElementById('show_inactive').checked) { | |
| url.searchParams.set('show_inactive', '1'); | |
| } else { | |
| url.searchParams.delete('show_inactive'); | |
| } | |
| window.location = url.toString(); | |
| } | |
| async function saveRow(id) { | |
| const code = document.getElementById('code_' + id).value.trim().toUpperCase(); | |
| const name = document.getElementById('name_' + id).value.trim(); | |
| const email = document.getElementById('email_' + id).value.trim(); | |
| const resp = await fetch('/admin/api/ces/' + id, { | |
| method: 'POST', headers: authHeaders, | |
| body: JSON.stringify({ code, name, email }), | |
| }); | |
| const data = await resp.json(); | |
| if (data.error) { alert(data.error); return; } | |
| flashRow(id, data.success); | |
| } | |
| async function toggleCE(id) { | |
| if (!confirm('Toggle active status for this client executive?')) return; | |
| await fetch('/admin/api/ces/' + id + '/toggle', { | |
| method: 'POST', headers: {'Authorization': 'Bearer ' + authToken}, | |
| }); | |
| location.reload(); | |
| } | |
| async function deleteCE(id, name) { | |
| if (!confirm('Permanently delete ' + name + '? This cannot be undone.')) return; | |
| const resp = await fetch('/admin/api/ces/' + id, { | |
| method: 'DELETE', headers: {'Authorization': 'Bearer ' + authToken}, | |
| }); | |
| const data = await resp.json(); | |
| if (data.success) location.reload(); | |
| else alert(data.error || 'Failed to delete'); | |
| } | |
| function flashRow(id, ok) { | |
| const input = document.getElementById('email_' + id); | |
| if (input) { | |
| input.style.outline = ok ? '2px solid #27ae60' : '2px solid #c0392b'; | |
| setTimeout(() => { input.style.outline = ''; }, 1000); | |
| } | |
| } | |
| function openAddModal() { document.getElementById('addModal').classList.remove('hidden'); } | |
| function closeAddModal() { document.getElementById('addModal').classList.add('hidden'); } | |
| async function createCE() { | |
| const body = { | |
| code: document.getElementById('new_code').value.trim().toUpperCase(), | |
| name: document.getElementById('new_name').value.trim(), | |
| email: document.getElementById('new_email').value.trim(), | |
| }; | |
| const resp = await fetch('/admin/api/ces/new', { | |
| method: 'POST', headers: authHeaders, | |
| body: JSON.stringify(body), | |
| }); | |
| const data = await resp.json(); | |
| if (data.success) { location.reload(); } | |
| else { alert(data.error || 'Failed to create CE'); } | |
| } | |
| async function bulkImport() { | |
| const text = document.getElementById('bulkInput').value.trim(); | |
| if (!text) return; | |
| const resp = await fetch('/admin/api/ces/bulk', { | |
| method: 'POST', headers: authHeaders, | |
| body: JSON.stringify({ text }), | |
| }); | |
| const data = await resp.json(); | |
| if (data.success) { | |
| document.getElementById('bulkResult').textContent = | |
| '✅ ' + data.added + ' added, ' + data.updated + ' updated' + | |
| (data.skipped ? ', ' + data.skipped + ' skipped' : ''); | |
| setTimeout(() => location.reload(), 1500); | |
| } else { | |
| document.getElementById('bulkResult').textContent = '❌ ' + (data.error || 'Failed'); | |
| } | |
| } | |
| </script> | |
| {% endblock %} | |