diff --git a/app/settings/frontend/settings.html b/app/settings/frontend/settings.html index db9d0a2..0e1cd01 100644 --- a/app/settings/frontend/settings.html +++ b/app/settings/frontend/settings.html @@ -89,6 +89,9 @@ Brugere + + Tags + AI Prompts @@ -183,6 +186,118 @@ + +
+
+
+
Tag Administration
+

Administrer tags der bruges på tværs af hele systemet

+
+ +
+ + +
+
+
+
+
0
+ Total Tags +
+
+
+
+
+
+
0
+ Workflow +
+
+
+
+
+
+
0
+ Status +
+
+
+
+
+
+
0
+ Category +
+
+
+
+
+
+
0
+ Priority +
+
+
+
+
+
+
0
+ Billing +
+
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+ + +
+
+
+
+
+
+
@@ -873,10 +988,305 @@ async function loadModules() { } } +// Tags Management +let allTagsData = []; +let currentTagFilter = 'all'; +let showInactive = false; + +async function loadTagsManagement() { + try { + const response = await fetch('/api/v1/tags'); + if (!response.ok) throw new Error('Failed to load tags'); + allTagsData = await response.json(); + updateTagsStats(); + renderTagsGrid(); + } catch (error) { + console.error('Error loading tags:', error); + showNotification('Fejl ved indlæsning af tags', 'error'); + } +} + +function updateTagsStats() { + const stats = { + total: allTagsData.length, + workflow: allTagsData.filter(t => t.type === 'workflow').length, + status: allTagsData.filter(t => t.type === 'status').length, + category: allTagsData.filter(t => t.type === 'category').length, + priority: allTagsData.filter(t => t.type === 'priority').length, + billing: allTagsData.filter(t => t.type === 'billing').length + }; + + document.getElementById('totalTagsCount').textContent = stats.total; + document.getElementById('workflowTagsCount').textContent = stats.workflow; + document.getElementById('statusTagsCount').textContent = stats.status; + document.getElementById('categoryTagsCount').textContent = stats.category; + document.getElementById('priorityTagsCount').textContent = stats.priority; + document.getElementById('billingTagsCount').textContent = stats.billing; +} + +function renderTagsGrid() { + const container = document.getElementById('tagsGrid'); + let tags = currentTagFilter === 'all' + ? allTagsData + : allTagsData.filter(t => t.type === currentTagFilter); + + if (!showInactive) { + tags = tags.filter(t => t.is_active); + } + + if (tags.length === 0) { + container.innerHTML = ` +
+ +

Ingen tags fundet

+
+ `; + return; + } + + container.innerHTML = tags.map(tag => ` +
+
+ ${!tag.is_active ? '
Inaktiv
' : ''} +
+
+
+
+ ${tag.icon ? `` : `${tag.name.charAt(0)}`} +
+
+
+
${tag.name}
+ ${tag.type} +
+
+ ${tag.description ? `

${tag.description}

` : '

Ingen beskrivelse

'} +
+ + +
+
+
+
+ `).join(''); +} + +function editTag(tagId) { + const tag = allTagsData.find(t => t.id === tagId); + if (!tag) return; + + document.getElementById('tagId').value = tag.id; + document.getElementById('tagName').value = tag.name; + document.getElementById('tagType').value = tag.type; + document.getElementById('tagDescription').value = tag.description || ''; + document.getElementById('tagColor').value = tag.color; + document.getElementById('tagColorHex').value = tag.color; + document.getElementById('tagIcon').value = tag.icon || ''; + document.getElementById('tagActive').checked = tag.is_active; + + document.querySelector('#tagModal .modal-title').textContent = 'Rediger Tag'; + new bootstrap.Modal(document.getElementById('tagModal')).show(); +} + +async function deleteTag(tagId, tagName) { + if (!confirm(`Slet tag "${tagName}"?\n\nDette vil også fjerne tagget fra alle steder det er brugt.`)) { + return; + } + + try { + const response = await fetch(`/api/v1/tags/${tagId}`, { + method: 'DELETE' + }); + + if (!response.ok) throw new Error('Failed to delete tag'); + showNotification(`Tag "${tagName}" slettet`, 'success'); + await loadTagsManagement(); + } catch (error) { + showNotification('Fejl ved sletning: ' + error.message, 'error'); + } +} + +async function saveTag() { + const tagId = document.getElementById('tagId').value; + const tagData = { + name: document.getElementById('tagName').value, + type: document.getElementById('tagType').value, + description: document.getElementById('tagDescription').value || null, + color: document.getElementById('tagColorHex').value, + icon: document.getElementById('tagIcon').value || null, + is_active: document.getElementById('tagActive').checked + }; + + try { + const url = tagId ? `/api/v1/tags/${tagId}` : '/api/v1/tags'; + const method = tagId ? 'PUT' : 'POST'; + + const response = await fetch(url, { + method: method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(tagData) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || 'Failed to save tag'); + } + + bootstrap.Modal.getInstance(document.getElementById('tagModal')).hide(); + showNotification(tagId ? 'Tag opdateret' : 'Tag oprettet', 'success'); + await loadTagsManagement(); + } catch (error) { + showNotification('Fejl: ' + error.message, 'error'); + } +} + +// Tag filter event listeners +document.querySelectorAll('#tagTypeFilter input[type="radio"]').forEach(radio => { + radio.addEventListener('change', (e) => { + currentTagFilter = e.target.value; + renderTagsGrid(); + }); +}); + +document.getElementById('showInactiveToggle').addEventListener('change', (e) => { + showInactive = e.target.checked; + renderTagsGrid(); +}); + +// Color picker sync +function setupTagModalListeners() { + const colorPicker = document.getElementById('tagColor'); + const colorHex = document.getElementById('tagColorHex'); + + if (colorPicker && colorHex) { + colorPicker.addEventListener('input', (e) => { + colorHex.value = e.target.value; + }); + + colorHex.addEventListener('input', (e) => { + const color = e.target.value; + if (/^#[0-9A-Fa-f]{6}$/.test(color)) { + colorPicker.value = color; + } + }); + } + + // Type change updates color + const tagType = document.getElementById('tagType'); + if (tagType) { + tagType.addEventListener('change', (e) => { + const type = e.target.value; + const colorMap = { + 'workflow': '#ff6b35', + 'status': '#ffd700', + 'category': '#0f4c75', + 'priority': '#dc3545', + 'billing': '#2d6a4f' + }; + if (colorMap[type] && colorPicker && colorHex) { + colorPicker.value = colorMap[type]; + colorHex.value = colorMap[type]; + } + }); + } + + // Modal reset on close + const tagModal = document.getElementById('tagModal'); + if (tagModal) { + tagModal.addEventListener('hidden.bs.modal', () => { + document.getElementById('tagForm').reset(); + document.getElementById('tagId').value = ''; + document.querySelector('#tagModal .modal-title').textContent = 'Opret Tag'; + }); + } +} + +// Load tags when tags tab is activated +const tagsNavLink = document.querySelector('a[data-tab="tags"]'); +if (tagsNavLink) { + tagsNavLink.addEventListener('click', () => { + if (allTagsData.length === 0) { + loadTagsManagement(); + } + }); +} + // Load on page ready document.addEventListener('DOMContentLoaded', () => { loadSettings(); loadUsers(); + setupTagModalListeners(); }); + + + + {% endblock %}