@@ -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 = `
+
+ `;
+ 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 %}