@@ -509,6 +604,137 @@
Import error log
+
+ No tags yet. Add one above to get started.
+
+ `;
+ return;
+ }
+
+ list.innerHTML = allTags.map(tag => {
+ const color = normalizeTagColor(tag.color);
+ const fg = contrastColor(color);
+ return `
+
+ `;
+ }).join('');
+
+ list.querySelectorAll('.tag-save-btn').forEach(btn => {
+ btn.addEventListener('click', async () => {
+ const item = btn.closest('.tag-manager-item');
+ const tagId = btn.dataset.tagId;
+ const name = item.querySelector('.tag-manager-name').value.trim();
+ const color = normalizeTagColor(item.querySelector('.tag-manager-color').value);
+
+ if (!name) {
+ setTagManagerStatus('Tag name cannot be empty.', 'danger');
+ item.querySelector('.tag-manager-name').focus();
+ return;
+ }
+
+ btn.disabled = true;
+ setTagManagerStatus('Saving tag changes...');
+ try {
+ const res = await fetchWithAuth(`/tags/${tagId}/`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name, color }),
+ });
+
+ if (!res.ok) {
+ throw new Error('Unable to update tag.');
+ }
+
+ setTagManagerStatus('Tag updated successfully.', 'success');
+ await loadTags();
+ } catch (error) {
+ console.error('Failed to update tag:', error);
+ setTagManagerStatus(error.message || 'Failed to update tag.', 'danger');
+ } finally {
+ btn.disabled = false;
+ }
+ });
+ });
+
+ list.querySelectorAll('.tag-delete-btn').forEach(btn => {
+ btn.addEventListener('click', async () => {
+ const tagId = btn.dataset.tagId;
+ const tag = allTags.find(item => String(item.id) === String(tagId));
+ const tagName = tag?.name || 'this tag';
+ if (!window.confirm(`Delete ${tagName}? Leads that use it will lose this tag.`)) {
+ return;
+ }
+
+ btn.disabled = true;
+ setTagManagerStatus('Deleting tag...');
+ try {
+ const res = await fetchWithAuth(`/tags/${tagId}/`, { method: 'DELETE' });
+ if (!res.ok && res.status !== 204) {
+ throw new Error('Unable to delete tag.');
+ }
+
+ selectedTagIds.delete(String(tagId));
+ setTagManagerStatus('Tag deleted.', 'success');
+ await loadTags();
+ } catch (error) {
+ console.error('Failed to delete tag:', error);
+ setTagManagerStatus(error.message || 'Failed to delete tag.', 'danger');
+ } finally {
+ btn.disabled = false;
+ }
+ });
+ });
+ }
+
// ─── Render ─────────────────────────────────────────────────────────
function renderImportErrorRows(job) {
@@ -642,7 +868,9 @@
No leads found
const res = await fetchWithAuth('/tags/');
if (!res.ok) return;
allTags = await res.json();
+ selectedTagIds = new Set([...selectedTagIds].filter(id => allTags.some(tag => String(tag.id) === String(id))));
renderTagFilterChips();
+ renderTagManager();
} catch (e) {
console.error('Failed to load tags:', e);
}
@@ -846,6 +1074,45 @@
No leads found
}
});
+ document.getElementById('create-tag-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const nameInput = document.getElementById('new-tag-name');
+ const colorInput = document.getElementById('new-tag-color');
+ const name = nameInput.value.trim();
+ const color = normalizeTagColor(colorInput.value);
+
+ if (!name) {
+ setTagManagerStatus('Tag name cannot be empty.', 'danger');
+ nameInput.focus();
+ return;
+ }
+
+ const createBtn = document.getElementById('create-tag-btn');
+ createBtn.disabled = true;
+ setTagManagerStatus('Creating tag...');
+ try {
+ const res = await fetchWithAuth('/tags/', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name, color }),
+ });
+
+ if (!res.ok) {
+ throw new Error('Unable to create tag.');
+ }
+
+ nameInput.value = '';
+ colorInput.value = '#6366f1';
+ setTagManagerStatus('Tag created successfully.', 'success');
+ await loadTags();
+ } catch (error) {
+ console.error('Failed to create tag:', error);
+ setTagManagerStatus(error.message || 'Failed to create tag.', 'danger');
+ } finally {
+ createBtn.disabled = false;
+ }
+ });
+
document.getElementById('upload-form').addEventListener('submit', async (e) => {
e.preventDefault();
const fileInput = document.getElementById('csv-file');
@@ -886,4 +1153,4 @@
No leads found