Spaces:
Running
Running
Upload producer_emails.html
Browse files
app/templates/admin/producer_emails.html
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Producer Emails — Admin{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="space-y-6">
|
| 7 |
+
<div class="flex items-center justify-between">
|
| 8 |
+
<div>
|
| 9 |
+
<h1 class="text-2xl font-bold sw-blue-text">Producer Email Addresses</h1>
|
| 10 |
+
<p class="text-gray-500 text-sm mt-1">
|
| 11 |
+
Assign email addresses to producers so they receive approval requests when listed on a submission.
|
| 12 |
+
</p>
|
| 13 |
+
</div>
|
| 14 |
+
<a href="/admin/?token={{ token }}" class="text-sm text-gray-400 hover:text-gray-600">← Back to Admin</a>
|
| 15 |
+
</div>
|
| 16 |
+
|
| 17 |
+
<!-- Search -->
|
| 18 |
+
<div class="bg-white rounded-lg shadow p-4">
|
| 19 |
+
<input type="text" id="searchBox" placeholder="Search by name or code…"
|
| 20 |
+
oninput="filterProducers()"
|
| 21 |
+
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">
|
| 22 |
+
<div class="flex items-center justify-between mt-3 text-xs text-gray-400">
|
| 23 |
+
<span id="countLabel">{{ producers | length }} producers</span>
|
| 24 |
+
<span>
|
| 25 |
+
<span class="inline-block w-3 h-3 rounded-full bg-green-400 mr-1"></span> Has email
|
| 26 |
+
<span class="inline-block w-3 h-3 rounded-full bg-gray-300 ml-3 mr-1"></span> No email
|
| 27 |
+
</span>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
|
| 31 |
+
<!-- Producer list -->
|
| 32 |
+
<div class="bg-white rounded-lg shadow overflow-hidden">
|
| 33 |
+
<table class="w-full text-sm">
|
| 34 |
+
<thead>
|
| 35 |
+
<tr class="sw-blue text-white text-left">
|
| 36 |
+
<th class="px-4 py-3 w-[5%]">#</th>
|
| 37 |
+
<th class="px-4 py-3 w-[15%]">Code</th>
|
| 38 |
+
<th class="px-4 py-3 w-[25%]">Name</th>
|
| 39 |
+
<th class="px-4 py-3 w-[35%]">Email Address</th>
|
| 40 |
+
<th class="px-4 py-3 w-[20%] text-center">Action</th>
|
| 41 |
+
</tr>
|
| 42 |
+
</thead>
|
| 43 |
+
<tbody id="producerTable">
|
| 44 |
+
{% for p in producers %}
|
| 45 |
+
<tr class="border-b hover:bg-gray-50 producer-row" data-id="{{ p.id }}" data-search="{{ p.name|lower }} {{ p.code|lower }}">
|
| 46 |
+
<td class="px-4 py-3 text-gray-400">{{ loop.index }}</td>
|
| 47 |
+
<td class="px-4 py-3 font-mono text-xs">
|
| 48 |
+
<span class="inline-block w-2 h-2 rounded-full mr-2 {{ 'bg-green-400' if p.email else 'bg-gray-300' }}"></span>
|
| 49 |
+
{{ p.code }}
|
| 50 |
+
</td>
|
| 51 |
+
<td class="px-4 py-3 font-semibold sw-blue-text">{{ p.name }}</td>
|
| 52 |
+
<td class="px-4 py-3">
|
| 53 |
+
<input type="email" id="email_{{ p.id }}"
|
| 54 |
+
value="{{ p.email or '' }}"
|
| 55 |
+
placeholder="name@snellingswalters.com"
|
| 56 |
+
class="w-full border border-gray-200 rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 {{ 'bg-green-50 border-green-200' if p.email else '' }}">
|
| 57 |
+
</td>
|
| 58 |
+
<td class="px-4 py-3 text-center">
|
| 59 |
+
<button onclick="saveEmail({{ p.id }})"
|
| 60 |
+
class="bg-blue-600 text-white px-4 py-1.5 rounded text-xs font-semibold hover:bg-blue-700 transition">
|
| 61 |
+
Save
|
| 62 |
+
</button>
|
| 63 |
+
{% if p.email %}
|
| 64 |
+
<button onclick="clearEmail({{ p.id }})"
|
| 65 |
+
class="text-red-400 hover:text-red-600 text-xs ml-2 transition">
|
| 66 |
+
Clear
|
| 67 |
+
</button>
|
| 68 |
+
{% endif %}
|
| 69 |
+
</td>
|
| 70 |
+
</tr>
|
| 71 |
+
{% endfor %}
|
| 72 |
+
</tbody>
|
| 73 |
+
</table>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<!-- Bulk paste -->
|
| 77 |
+
<div class="bg-white rounded-lg shadow p-5">
|
| 78 |
+
<h3 class="font-semibold sw-blue-text mb-2">Bulk Import</h3>
|
| 79 |
+
<p class="text-gray-400 text-xs mb-3">Paste CSV lines: <code>PRODUCER_CODE,email@example.com</code> (one per line)</p>
|
| 80 |
+
<textarea id="bulkInput" rows="4" placeholder="KOPAL1,akops@snellingswalters.com SNEBL1,bsnellings@snellingswalters.com"
|
| 81 |
+
class="w-full border border-gray-200 rounded-lg px-4 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-400"></textarea>
|
| 82 |
+
<button onclick="bulkImport()"
|
| 83 |
+
class="mt-2 bg-green-600 text-white px-6 py-2 rounded-lg text-sm font-semibold hover:bg-green-700 transition">
|
| 84 |
+
Import All
|
| 85 |
+
</button>
|
| 86 |
+
<span id="bulkResult" class="ml-3 text-sm text-gray-500"></span>
|
| 87 |
+
</div>
|
| 88 |
+
</div>
|
| 89 |
+
{% endblock %}
|
| 90 |
+
|
| 91 |
+
{% block scripts %}
|
| 92 |
+
<script>
|
| 93 |
+
const authToken = '{{ token }}';
|
| 94 |
+
|
| 95 |
+
function filterProducers() {
|
| 96 |
+
const q = document.getElementById('searchBox').value.toLowerCase();
|
| 97 |
+
const rows = document.querySelectorAll('.producer-row');
|
| 98 |
+
let visible = 0;
|
| 99 |
+
rows.forEach(r => {
|
| 100 |
+
const match = r.dataset.search.includes(q);
|
| 101 |
+
r.style.display = match ? '' : 'none';
|
| 102 |
+
if (match) visible++;
|
| 103 |
+
});
|
| 104 |
+
document.getElementById('countLabel').textContent = visible + ' producers shown';
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
async function saveEmail(producerId) {
|
| 108 |
+
const input = document.getElementById('email_' + producerId);
|
| 109 |
+
const email = input.value.trim();
|
| 110 |
+
|
| 111 |
+
try {
|
| 112 |
+
const resp = await fetch('/admin/api/producers/' + producerId + '/email', {
|
| 113 |
+
method: 'POST',
|
| 114 |
+
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken},
|
| 115 |
+
body: JSON.stringify({ email: email }),
|
| 116 |
+
});
|
| 117 |
+
const data = await resp.json();
|
| 118 |
+
if (data.success) {
|
| 119 |
+
input.className = input.className.replace('bg-green-50 border-green-200', '').trim();
|
| 120 |
+
if (email) {
|
| 121 |
+
input.className += ' bg-green-50 border-green-200';
|
| 122 |
+
}
|
| 123 |
+
// Flash green
|
| 124 |
+
input.style.outline = '2px solid #27ae60';
|
| 125 |
+
setTimeout(() => { input.style.outline = ''; }, 1000);
|
| 126 |
+
} else {
|
| 127 |
+
alert('Error: ' + (data.error || 'Unknown'));
|
| 128 |
+
}
|
| 129 |
+
} catch (e) {
|
| 130 |
+
alert('Save failed: ' + e.message);
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
async function clearEmail(producerId) {
|
| 135 |
+
const input = document.getElementById('email_' + producerId);
|
| 136 |
+
input.value = '';
|
| 137 |
+
await saveEmail(producerId);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
async function bulkImport() {
|
| 141 |
+
const text = document.getElementById('bulkInput').value.trim();
|
| 142 |
+
if (!text) return;
|
| 143 |
+
|
| 144 |
+
const lines = text.split('\n').filter(l => l.trim());
|
| 145 |
+
let success = 0, failed = 0;
|
| 146 |
+
|
| 147 |
+
for (const line of lines) {
|
| 148 |
+
const parts = line.split(',').map(s => s.trim());
|
| 149 |
+
if (parts.length < 2) { failed++; continue; }
|
| 150 |
+
|
| 151 |
+
try {
|
| 152 |
+
const resp = await fetch('/admin/api/producers/email-by-code', {
|
| 153 |
+
method: 'POST',
|
| 154 |
+
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken},
|
| 155 |
+
body: JSON.stringify({ code: parts[0], email: parts[1] }),
|
| 156 |
+
});
|
| 157 |
+
const data = await resp.json();
|
| 158 |
+
if (data.success) success++;
|
| 159 |
+
else failed++;
|
| 160 |
+
} catch (e) { failed++; }
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
document.getElementById('bulkResult').textContent =
|
| 164 |
+
'✅ ' + success + ' saved, ' + (failed > 0 ? '❌ ' + failed + ' failed' : 'all good!');
|
| 165 |
+
|
| 166 |
+
// Reload to reflect changes
|
| 167 |
+
if (success > 0) {
|
| 168 |
+
setTimeout(() => location.reload(), 1500);
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
</script>
|
| 172 |
+
{% endblock %}
|