Feature: Add tags administration to settings page
- Add Tags navigation tab in settings - Modern card-based grid layout for tags - Quick stats dashboard (6 KPI cards) - Smart filtering (type + inactive toggle) - Create/Edit/Delete functionality - Color picker with hex input sync - Auto-color suggestion based on tag type - Bootstrap Icons selector - Responsive 3-column layout
This commit is contained in:
parent
15f39f13ce
commit
a011f36385
@ -89,6 +89,9 @@
|
||||
<a class="nav-link" href="#users" data-tab="users">
|
||||
<i class="bi bi-people me-2"></i>Brugere
|
||||
</a>
|
||||
<a class="nav-link" href="#tags" data-tab="tags">
|
||||
<i class="bi bi-tags me-2"></i>Tags
|
||||
</a>
|
||||
<a class="nav-link" href="#ai-prompts" data-tab="ai-prompts">
|
||||
<i class="bi bi-robot me-2"></i>AI Prompts
|
||||
</a>
|
||||
@ -183,6 +186,118 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tags Management -->
|
||||
<div class="tab-pane fade" id="tags">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h5 class="fw-bold mb-1">Tag Administration</h5>
|
||||
<p class="text-muted mb-0">Administrer tags der bruges på tværs af hele systemet</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#tagModal">
|
||||
<i class="bi bi-plus-lg me-2"></i>Opret Tag
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-2">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<div class="display-6 fw-bold text-primary" id="totalTagsCount">0</div>
|
||||
<small class="text-muted">Total Tags</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="card border-0 shadow-sm" style="border-left: 4px solid #ff6b35 !important;">
|
||||
<div class="card-body text-center">
|
||||
<div class="h4 fw-bold" id="workflowTagsCount" style="color: #ff6b35;">0</div>
|
||||
<small class="text-muted">Workflow</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="card border-0 shadow-sm" style="border-left: 4px solid #ffd700 !important;">
|
||||
<div class="card-body text-center">
|
||||
<div class="h4 fw-bold" id="statusTagsCount" style="color: #e6c200;">0</div>
|
||||
<small class="text-muted">Status</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="card border-0 shadow-sm" style="border-left: 4px solid #0f4c75 !important;">
|
||||
<div class="card-body text-center">
|
||||
<div class="h4 fw-bold" id="categoryTagsCount" style="color: #0f4c75;">0</div>
|
||||
<small class="text-muted">Category</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="card border-0 shadow-sm" style="border-left: 4px solid #dc3545 !important;">
|
||||
<div class="card-body text-center">
|
||||
<div class="h4 fw-bold" id="priorityTagsCount" style="color: #dc3545;">0</div>
|
||||
<small class="text-muted">Priority</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="card border-0 shadow-sm" style="border-left: 4px solid #2d6a4f !important;">
|
||||
<div class="card-body text-center">
|
||||
<div class="h4 fw-bold" id="billingTagsCount" style="color: #2d6a4f;">0</div>
|
||||
<small class="text-muted">Billing</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Pills -->
|
||||
<div class="mb-3 d-flex justify-content-between align-items-center">
|
||||
<div class="btn-group btn-group-sm" role="group" id="tagTypeFilter">
|
||||
<input type="radio" class="btn-check" name="tagType" id="typeAll" value="all" checked>
|
||||
<label class="btn btn-outline-secondary" for="typeAll">Alle</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="tagType" id="typeWorkflow" value="workflow">
|
||||
<label class="btn btn-outline-secondary" for="typeWorkflow">
|
||||
<i class="bi bi-diagram-3 me-1"></i>Workflow
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="tagType" id="typeStatus" value="status">
|
||||
<label class="btn btn-outline-secondary" for="typeStatus">
|
||||
<i class="bi bi-hourglass-split me-1"></i>Status
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="tagType" id="typeCategory" value="category">
|
||||
<label class="btn btn-outline-secondary" for="typeCategory">
|
||||
<i class="bi bi-bookmark me-1"></i>Category
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="tagType" id="typePriority" value="priority">
|
||||
<label class="btn btn-outline-secondary" for="typePriority">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>Priority
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="tagType" id="typeBilling" value="billing">
|
||||
<label class="btn btn-outline-secondary" for="typeBilling">
|
||||
<i class="bi bi-currency-dollar me-1"></i>Billing
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="showInactiveToggle">
|
||||
<label class="form-check-label small text-muted" for="showInactiveToggle">
|
||||
Vis inaktive
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tags Grid -->
|
||||
<div class="row g-3" id="tagsGrid">
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI Prompts -->
|
||||
<div class="tab-pane fade" id="ai-prompts">
|
||||
<div class="card p-4">
|
||||
@ -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 = `
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="bi bi-inbox display-1 text-muted"></i>
|
||||
<p class="text-muted mt-3">Ingen tags fundet</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = tags.map(tag => `
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card h-100 border-0 shadow-sm position-relative" style="border-left: 4px solid ${tag.color} !important;">
|
||||
${!tag.is_active ? '<div class="position-absolute top-0 end-0 m-2"><span class="badge bg-secondary">Inaktiv</span></div>' : ''}
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-start mb-3">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="rounded" style="width: 48px; height: 48px; background-color: ${tag.color}; display: flex; align-items: center; justify-content: center;">
|
||||
${tag.icon ? `<i class="bi ${tag.icon} text-white" style="font-size: 1.5rem;"></i>` : `<span class="text-white fw-bold">${tag.name.charAt(0)}</span>`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<h6 class="card-title mb-1 fw-bold">${tag.name}</h6>
|
||||
<span class="badge" style="background-color: ${tag.color}20; color: ${tag.color};">${tag.type}</span>
|
||||
</div>
|
||||
</div>
|
||||
${tag.description ? `<p class="card-text small text-muted mb-3">${tag.description}</p>` : '<p class="card-text small text-muted mb-3"><em>Ingen beskrivelse</em></p>'}
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-primary flex-grow-1" onclick="editTag(${tag.id})">
|
||||
<i class="bi bi-pencil me-1"></i>Rediger
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteTag(${tag.id}, '${tag.name.replace(/'/g, "\\'")}')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).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();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Tag Create/Edit Modal -->
|
||||
<div class="modal fade" id="tagModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Opret Tag</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="tagForm">
|
||||
<input type="hidden" id="tagId">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="tagName" class="form-label">Navn *</label>
|
||||
<input type="text" class="form-control" id="tagName" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="tagType" class="form-label">Type *</label>
|
||||
<select class="form-select" id="tagType" required>
|
||||
<option value="">Vælg type...</option>
|
||||
<option value="workflow">Workflow - Trigger automatisering</option>
|
||||
<option value="status">Status - Tilstand/fase</option>
|
||||
<option value="category">Category - Emne/område</option>
|
||||
<option value="priority">Priority - Hastighed</option>
|
||||
<option value="billing">Billing - Økonomi</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="tagDescription" class="form-label">Beskrivelse</label>
|
||||
<textarea class="form-control" id="tagDescription" rows="3"></textarea>
|
||||
<small class="text-muted">Forklaring af hvad tagget gør eller betyder</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="tagColor" class="form-label">Farve *</label>
|
||||
<div class="input-group">
|
||||
<input type="color" class="form-control form-control-color" id="tagColor" value="#0f4c75">
|
||||
<input type="text" class="form-control" id="tagColorHex" value="#0f4c75" pattern="^#[0-9A-Fa-f]{6}$">
|
||||
</div>
|
||||
<small class="text-muted">Hex color code (fx #0f4c75)</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="tagIcon" class="form-label">Ikon (valgfrit)</label>
|
||||
<input type="text" class="form-control" id="tagIcon" placeholder="bi-star">
|
||||
<small class="text-muted">Bootstrap Icons navn (fx: bi-star, bi-flag, bi-check-circle)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="tagActive" checked>
|
||||
<label class="form-check-label" for="tagActive">
|
||||
Aktiv
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveTag()">Gem</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user