bmc_hub/app/settings/frontend/settings.html

1731 lines
73 KiB
HTML

{% extends "shared/frontend/base.html" %}
{% block title %}Indstillinger - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.settings-nav {
position: sticky;
top: 100px;
}
.settings-nav .nav-link {
color: var(--text-secondary);
padding: 0.75rem 1rem;
border-radius: 8px;
margin-bottom: 0.5rem;
transition: all 0.2s;
border-left: 3px solid transparent;
}
.settings-nav .nav-link:hover,
.settings-nav .nav-link.active {
background: var(--accent-light);
color: var(--accent);
border-left-color: var(--accent);
}
.setting-group {
margin-bottom: 2rem;
}
.setting-item {
padding: 1.25rem;
border-bottom: 1px solid rgba(0,0,0,0.05);
display: flex;
justify-content: space-between;
align-items: center;
}
.setting-item:last-child {
border-bottom: none;
}
.setting-info h6 {
margin-bottom: 0.25rem;
font-weight: 600;
}
.setting-info small {
color: var(--text-secondary);
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--accent-light);
color: var(--accent);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
</style>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h2 class="fw-bold mb-1">Indstillinger</h2>
<p class="text-muted mb-0">System konfiguration og brugerstyring</p>
</div>
</div>
<div class="row">
<!-- Vertical Navigation -->
<div class="col-lg-2">
<div class="settings-nav">
<nav class="nav flex-column">
<a class="nav-link active" href="#company" data-tab="company">
<i class="bi bi-building me-2"></i>Firma
</a>
<a class="nav-link" href="#integrations" data-tab="integrations">
<i class="bi bi-plugin me-2"></i>Integrationer
</a>
<a class="nav-link" href="#notifications" data-tab="notifications">
<i class="bi bi-bell me-2"></i>Notifikationer
</a>
<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="#sync" data-tab="sync">
<i class="bi bi-arrow-repeat me-2"></i>Sync
</a>
<a class="nav-link" href="#ai-prompts" data-tab="ai-prompts">
<i class="bi bi-robot me-2"></i>AI Prompts
</a>
<a class="nav-link" href="#modules" data-tab="modules">
<i class="bi bi-box-seam me-2"></i>Moduler
</a>
<a class="nav-link" href="#system" data-tab="system">
<i class="bi bi-gear me-2"></i>System
</a>
</nav>
</div>
</div>
<!-- Content Area -->
<div class="col-lg-10">
<div class="tab-content">
<!-- Company Settings -->
<div class="tab-pane fade show active" id="company">
<div class="card p-4">
<h5 class="mb-4 fw-bold">Firma Oplysninger</h5>
<div id="companySettings">
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status"></div>
</div>
</div>
</div>
</div>
<!-- Integrations -->
<div class="tab-pane fade" id="integrations">
<div class="card p-4 mb-4">
<h5 class="mb-4 fw-bold">vTiger CRM</h5>
<div id="vtigerSettings">
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status"></div>
</div>
</div>
</div>
<div class="card p-4">
<h5 class="mb-4 fw-bold">e-conomic</h5>
<div id="economicSettings">
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status"></div>
</div>
</div>
</div>
</div>
<!-- Notifications -->
<div class="tab-pane fade" id="notifications">
<div class="card p-4">
<h5 class="mb-4 fw-bold">Notifikation Indstillinger</h5>
<div id="notificationSettings">
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status"></div>
</div>
</div>
</div>
</div>
<!-- Users -->
<div class="tab-pane fade" id="users">
<div class="card p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="mb-0 fw-bold">Brugerstyring</h5>
<button class="btn btn-primary" onclick="showCreateUserModal()">
<i class="bi bi-plus-lg me-2"></i>Opret Bruger
</button>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Bruger</th>
<th>Email</th>
<th>Status</th>
<th>Sidst Login</th>
<th>Oprettet</th>
<th class="text-end">Handlinger</th>
</tr>
</thead>
<tbody id="usersTableBody">
<tr>
<td colspan="6" class="text-center py-5">
<div class="spinner-border text-primary" role="status"></div>
</td>
</tr>
</tbody>
</table>
</div>
</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>
<!-- Sync Integration -->
<div class="tab-pane fade" id="sync">
<div class="mb-4">
<h5 class="fw-bold mb-1">Data Synkronisering</h5>
<p class="text-muted mb-0">Synkroniser firmaer og kontakter fra vTiger og e-conomic</p>
</div>
<!-- Sync Status Cards -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="rounded-circle" style="width: 48px; height: 48px; background: #f0f9ff; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-building text-primary" style="font-size: 1.5rem;"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="small text-muted">Firmaer i Hub</div>
<div class="h4 mb-0" id="syncStatsCustomers">-</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="rounded-circle" style="width: 48px; height: 48px; background: #fff4ed; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-diagram-3 text-warning" style="font-size: 1.5rem;"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="small text-muted">Med vTiger ID</div>
<div class="h4 mb-0" id="syncStatsVtiger">-</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="rounded-circle" style="width: 48px; height: 48px; background: #f0fdf4; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-currency-dollar text-success" style="font-size: 1.5rem;"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="small text-muted">Med e-conomic ID</div>
<div class="h4 mb-0" id="syncStatsEconomic">-</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Sync Actions -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-body">
<div class="d-flex align-items-start mb-3">
<div class="flex-shrink-0">
<i class="bi bi-diagram-3 text-warning" style="font-size: 2rem;"></i>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="card-title fw-bold">Sync fra vTiger</h6>
<p class="card-text small text-muted">Hent firmaer og kontakter fra vTiger CRM. Matcher på CVR nummer eller firma navn.</p>
</div>
</div>
<div class="d-grid gap-2">
<button class="btn btn-warning" onclick="syncFromVtiger()" id="btnSyncVtiger">
<i class="bi bi-download me-2"></i>Sync Firmaer fra vTiger
</button>
<button class="btn btn-outline-warning btn-sm" onclick="syncVtigerContacts()" id="btnSyncVtigerContacts">
<i class="bi bi-people me-2"></i>Sync Kontakter fra vTiger
</button>
</div>
<div class="mt-3 small">
<div class="d-flex align-items-center text-muted">
<i class="bi bi-info-circle me-2"></i>
<span>Sidst synkroniseret: <span id="lastSyncVtiger">Aldrig</span></span>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-body">
<div class="d-flex align-items-start mb-3">
<div class="flex-shrink-0">
<i class="bi bi-currency-dollar text-success" style="font-size: 2rem;"></i>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="card-title fw-bold">Sync fra e-conomic</h6>
<p class="card-text small text-muted">Hent kundenumre fra e-conomic. Matcher på CVR nummer eller firma navn.</p>
</div>
</div>
<div class="d-grid gap-2">
<button class="btn btn-success" onclick="syncFromEconomic()" id="btnSyncEconomic">
<i class="bi bi-download me-2"></i>Sync Firmaer fra e-conomic
</button>
<button class="btn btn-outline-success btn-sm" onclick="syncCvrToEconomic()" id="btnSyncCvrEconomic">
<i class="bi bi-search me-2"></i>Find Manglende CVR i e-conomic
</button>
</div>
<div class="mt-3 small">
<div class="d-flex align-items-center text-muted">
<i class="bi bi-info-circle me-2"></i>
<span>Sidst synkroniseret: <span id="lastSyncEconomic">Aldrig</span></span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Sync Log -->
<div class="card">
<div class="card-header bg-white">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold">Synkroniserings Log</h6>
<button class="btn btn-sm btn-outline-secondary" onclick="loadSyncLog()">
<i class="bi bi-arrow-clockwise me-1"></i>Opdater
</button>
</div>
</div>
<div class="card-body p-0">
<div id="syncLogContainer" style="max-height: 400px; overflow-y: auto;">
<div class="text-center py-5">
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
<p class="text-muted small mt-2 mb-0">Indlæser log...</p>
</div>
</div>
</div>
</div>
</div>
<!-- /div>
</div>
<!-- AI Prompts -->
<div class="tab-pane fade" id="ai-prompts">
<div class="card p-4">
<h5 class="mb-4 fw-bold">
<i class="bi bi-robot me-2"></i>AI System Prompts
</h5>
<p class="text-muted mb-4">
Her kan du se de prompts der bruges til forskellige AI funktioner i systemet.
</p>
<div id="aiPromptsContent">
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status"></div>
</div>
</div>
</div>
</div>
<!-- Modules Documentation -->
<div class="tab-pane fade" id="modules">
<div class="card p-4">
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<h5 class="fw-bold mb-2">📦 Modul System</h5>
<p class="text-muted mb-0">Dynamisk feature loading - udvikl moduler isoleret fra core systemet</p>
</div>
<!-- <a href="/api/v1/modules" target="_blank" class="btn btn-sm btn-outline-primary">
<i class="bi bi-box-arrow-up-right me-1"></i>API
</a> -->
<span class="badge bg-secondary">API ikke implementeret endnu</span>
</div>
<!-- Quick Start -->
<div class="alert alert-info">
<h6 class="alert-heading"><i class="bi bi-rocket me-2"></i>Quick Start</h6>
<p class="mb-2">Opret nyt modul på 5 minutter:</p>
<pre class="bg-white p-3 rounded mb-2" style="font-size: 0.85rem;"><code># 1. Opret modul
python3 scripts/create_module.py invoice_scanner "Scan fakturaer"
# 2. Kør migration
docker-compose exec db psql -U bmc_hub -d bmc_hub \\
-f app/modules/invoice_scanner/migrations/001_init.sql
# 3. Enable modul (rediger module.json)
"enabled": true
# 4. Restart API
docker-compose restart api
# 5. Test
curl http://localhost:8000/api/v1/invoice_scanner/health</code></pre>
</div>
<!-- Active Modules Status -->
<div class="card mb-4">
<div class="card-header bg-white">
<h6 class="mb-0 fw-bold">Aktive Moduler</h6>
</div>
<div class="card-body" id="activeModules">
<div class="text-center py-3">
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
<p class="text-muted small mt-2 mb-0">Indlæser moduler...</p>
</div>
</div>
</div>
<!-- Features -->
<div class="row mb-4">
<div class="col-md-6 mb-3">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body">
<h6 class="fw-bold mb-3"><i class="bi bi-shield-check text-success me-2"></i>Safety First</h6>
<ul class="list-unstyled mb-0">
<li class="mb-2">✅ Moduler starter disabled</li>
<li class="mb-2">✅ READ_ONLY og DRY_RUN defaults</li>
<li class="mb-2">✅ Error isolation - crashes påvirker ikke core</li>
<li class="mb-0">✅ Graceful degradation</li>
</ul>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body">
<h6 class="fw-bold mb-3"><i class="bi bi-database text-primary me-2"></i>Database Isolering</h6>
<ul class="list-unstyled mb-0">
<li class="mb-2">✅ Table prefix pattern (fx <code>mymod_customers</code>)</li>
<li class="mb-2">✅ Separate migration tracking</li>
<li class="mb-2">✅ Helper functions til queries</li>
<li class="mb-0">✅ Core database uberørt</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Module Structure -->
<div class="card mb-4">
<div class="card-header bg-white">
<h6 class="mb-0 fw-bold">Modul Struktur</h6>
</div>
<div class="card-body">
<pre class="bg-light p-3 rounded mb-0" style="font-size: 0.85rem;"><code>app/modules/my_module/
├── module.json # Metadata og konfiguration
├── README.md # Dokumentation
├── backend/
│ ├── __init__.py
│ └── router.py # FastAPI endpoints (API)
├── frontend/
│ ├── __init__.py
│ └── views.py # HTML view routes
├── templates/
│ └── index.html # Jinja2 templates
└── migrations/
└── 001_init.sql # Database migrations</code></pre>
</div>
</div>
<!-- Configuration Pattern -->
<div class="card mb-4">
<div class="card-header bg-white">
<h6 class="mb-0 fw-bold">Konfiguration</h6>
</div>
<div class="card-body">
<p class="text-muted mb-3">Modul-specifik konfiguration i <code>.env</code>:</p>
<pre class="bg-light p-3 rounded mb-3" style="font-size: 0.85rem;"><code># Pattern: MODULES__{MODULE_NAME}__{KEY}
MODULES__MY_MODULE__API_KEY=secret123
MODULES__MY_MODULE__READ_ONLY=false
MODULES__MY_MODULE__DRY_RUN=false</code></pre>
<p class="text-muted mb-2">I kode:</p>
<pre class="bg-light p-3 rounded mb-0" style="font-size: 0.85rem;"><code>from app.core.config import get_module_config
api_key = get_module_config("my_module", "API_KEY")
read_only = get_module_config("my_module", "READ_ONLY", "true")</code></pre>
</div>
</div>
<!-- Code Example -->
<div class="card mb-4">
<div class="card-header bg-white">
<h6 class="mb-0 fw-bold">Eksempel: API Endpoint</h6>
</div>
<div class="card-body">
<pre class="bg-light p-3 rounded mb-0" style="font-size: 0.85rem;"><code>from fastapi import APIRouter, HTTPException
from app.core.database import execute_query, execute_insert
from app.core.config import get_module_config
router = APIRouter()
@router.post("/my_module/scan")
async def scan_document(file_path: str):
"""Scan et dokument"""
# Safety check
read_only = get_module_config("my_module", "READ_ONLY", "true")
if read_only == "true":
return {"error": "READ_ONLY mode enabled"}
# Process document
result = process_file(file_path)
# Gem i database (bemærk table prefix!)
doc_id = execute_insert(
"INSERT INTO mymod_documents (path, result) VALUES (%s, %s)",
(file_path, result)
)
return {"success": True, "doc_id": doc_id}</code></pre>
</div>
</div>
<!-- Documentation Links -->
<div class="card border-primary">
<div class="card-header bg-primary text-white">
<h6 class="mb-0 fw-bold"><i class="bi bi-book me-2"></i>Dokumentation</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4 mb-3">
<h6 class="fw-bold">Quick Start</h6>
<p class="small text-muted mb-2">5 minutter guide til at komme i gang</p>
<code class="d-block small">docs/MODULE_QUICKSTART.md</code>
</div>
<div class="col-md-4 mb-3">
<h6 class="fw-bold">Full Guide</h6>
<p class="small text-muted mb-2">Komplet reference (6000+ ord)</p>
<code class="d-block small">docs/MODULE_SYSTEM.md</code>
</div>
<div class="col-md-4 mb-3">
<h6 class="fw-bold">Template</h6>
<p class="small text-muted mb-2">Working example modul</p>
<code class="d-block small">app/modules/_template/</code>
</div>
</div>
</div>
</div>
<!-- Best Practices -->
<div class="row mt-4">
<div class="col-md-6">
<div class="card border-success">
<div class="card-header bg-success text-white">
<h6 class="mb-0 fw-bold">✅ DO</h6>
</div>
<div class="card-body">
<ul class="mb-0 small">
<li>Brug <code>create_module.py</code> CLI tool</li>
<li>Brug table prefix konsistent</li>
<li>Enable safety switches i development</li>
<li>Test isoleret før enable i production</li>
<li>Log med emoji prefix (🔄 ✅ ❌)</li>
<li>Dokumenter API endpoints</li>
<li>Version moduler semantisk</li>
</ul>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h6 class="mb-0 fw-bold">❌ DON'T</h6>
</div>
<div class="card-body">
<ul class="mb-0 small">
<li>Skip table prefix</li>
<li>Hardcode credentials</li>
<li>Disable safety uden grund</li>
<li>Tilgå andre modulers tabeller direkte</li>
<li>Glem at køre migrations</li>
<li>Commit <code>.env</code> files</li>
<li>Enable direkte i production</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- System Settings -->
<div class="tab-pane fade" id="system">
<div class="card p-4">
<h5 class="mb-4 fw-bold">System Indstillinger</h5>
<div id="systemSettings">
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Create User Modal -->
<div class="modal fade" id="createUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Opret Ny Bruger</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="createUserForm">
<div class="mb-3">
<label class="form-label">Brugernavn *</label>
<input type="text" class="form-control" id="newUsername" required>
</div>
<div class="mb-3">
<label class="form-label">Email *</label>
<input type="email" class="form-control" id="newEmail" required>
</div>
<div class="mb-3">
<label class="form-label">Fulde Navn</label>
<input type="text" class="form-control" id="newFullName">
</div>
<div class="mb-3">
<label class="form-label">Adgangskode *</label>
<input type="password" class="form-control" id="newPassword" required>
</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="createUser()">Opret Bruger</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let allSettings = [];
async function loadSettings() {
try {
const response = await fetch('/api/v1/settings');
allSettings = await response.json();
displaySettingsByCategory();
} catch (error) {
console.error('Error loading settings:', error);
}
}
function displaySettingsByCategory() {
const categories = {
company: ['company_name', 'company_cvr', 'company_email', 'company_phone', 'company_address'],
integrations: ['vtiger_enabled', 'vtiger_url', 'vtiger_username', 'economic_enabled', 'economic_app_secret', 'economic_agreement_token'],
notifications: ['email_notifications'],
system: ['system_timezone']
};
// Company settings
displaySettings('companySettings', categories.company);
// vTiger settings
displaySettings('vtigerSettings', ['vtiger_enabled', 'vtiger_url', 'vtiger_username']);
// Economic settings
displaySettings('economicSettings', ['economic_enabled', 'economic_app_secret', 'economic_agreement_token']);
// Notification settings
displaySettings('notificationSettings', categories.notifications);
// System settings
displaySettings('systemSettings', categories.system);
}
function displaySettings(containerId, keys) {
const container = document.getElementById(containerId);
const settings = allSettings.filter(s => keys.includes(s.key));
if (settings.length === 0) {
container.innerHTML = '<p class="text-muted">Ingen indstillinger tilgængelige</p>';
return;
}
container.innerHTML = settings.map(setting => {
const inputId = `setting_${setting.key}`;
let inputHtml = '';
if (setting.value_type === 'boolean') {
inputHtml = `
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="${inputId}"
${setting.value === 'true' ? 'checked' : ''}
onchange="updateSetting('${setting.key}', this.checked ? 'true' : 'false')">
</div>
`;
} else if (setting.key.includes('password') || setting.key.includes('secret') || setting.key.includes('token')) {
inputHtml = `
<input type="password" class="form-control" id="${inputId}"
value="${setting.value || ''}"
onblur="updateSetting('${setting.key}', this.value)"
style="max-width: 300px;">
`;
} else {
inputHtml = `
<input type="text" class="form-control" id="${inputId}"
value="${setting.value || ''}"
onblur="updateSetting('${setting.key}', this.value)"
style="max-width: 300px;">
`;
}
return `
<div class="setting-item">
<div class="setting-info">
<h6>${setting.description || setting.key}</h6>
<small><code>${setting.key}</code></small>
</div>
<div>${inputHtml}</div>
</div>
`;
}).join('');
}
async function updateSetting(key, value) {
try {
const response = await fetch(`/api/v1/settings/${key}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ value })
});
if (response.ok) {
// Show success toast
console.log(`✅ Updated ${key}`);
}
} catch (error) {
console.error('Error updating setting:', error);
alert('Kunne ikke opdatere indstilling');
}
}
async function loadUsers() {
try {
const response = await fetch('/api/v1/users');
const users = await response.json();
displayUsers(users);
} catch (error) {
console.error('Error loading users:', error);
}
}
function displayUsers(users) {
const tbody = document.getElementById('usersTableBody');
if (users.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted py-5">Ingen brugere fundet</td></tr>';
return;
}
tbody.innerHTML = users.map(user => `
<tr>
<td>
<div class="d-flex align-items-center gap-3">
<div class="user-avatar">${getInitials(user.username)}</div>
<div>
<div class="fw-semibold">${escapeHtml(user.username)}</div>
${user.full_name ? `<small class="text-muted">${escapeHtml(user.full_name)}</small>` : ''}
</div>
</div>
</td>
<td>${user.email ? escapeHtml(user.email) : '<span class="text-muted">-</span>'}</td>
<td>
<span class="badge ${user.is_active ? 'bg-success' : 'bg-secondary'}">
${user.is_active ? 'Aktiv' : 'Inaktiv'}
</span>
</td>
<td>${user.last_login ? formatDate(user.last_login) : '<span class="text-muted">Aldrig</span>'}</td>
<td>${formatDate(user.created_at)}</td>
<td class="text-end">
<div class="btn-group btn-group-sm">
<button class="btn btn-light" onclick="resetPassword(${user.id})" title="Nulstil adgangskode">
<i class="bi bi-key"></i>
</button>
<button class="btn btn-light" onclick="toggleUserActive(${user.id}, ${!user.is_active})"
title="${user.is_active ? 'Deaktiver' : 'Aktiver'}">
<i class="bi bi-${user.is_active ? 'pause' : 'play'}-circle"></i>
</button>
</div>
</td>
</tr>
`).join('');
}
function showCreateUserModal() {
const modal = new bootstrap.Modal(document.getElementById('createUserModal'));
modal.show();
}
async function createUser() {
const user = {
username: document.getElementById('newUsername').value,
email: document.getElementById('newEmail').value,
full_name: document.getElementById('newFullName').value || null,
password: document.getElementById('newPassword').value
};
try {
const response = await fetch('/api/v1/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
});
if (response.ok) {
bootstrap.Modal.getInstance(document.getElementById('createUserModal')).hide();
document.getElementById('createUserForm').reset();
loadUsers();
} else {
const error = await response.json();
alert(error.detail || 'Kunne ikke oprette bruger');
}
} catch (error) {
console.error('Error creating user:', error);
alert('Kunne ikke oprette bruger');
}
}
async function toggleUserActive(userId, isActive) {
try {
const response = await fetch(`/api/v1/users/${userId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_active: isActive })
});
if (response.ok) {
loadUsers();
}
} catch (error) {
console.error('Error toggling user:', error);
}
}
async function resetPassword(userId) {
const newPassword = prompt('Indtast ny adgangskode:');
if (!newPassword) return;
try {
const response = await fetch(`/api/v1/users/${userId}/reset-password?new_password=${newPassword}`, {
method: 'POST'
});
if (response.ok) {
alert('Adgangskode nulstillet!');
}
} catch (error) {
console.error('Error resetting password:', error);
alert('Kunne ikke nulstille adgangskode');
}
}
function getInitials(name) {
return name.split(' ').map(word => word[0]).join('').substring(0, 2).toUpperCase();
}
// Load AI Prompts
async function loadAIPrompts() {
try {
const response = await fetch('/api/v1/ai-prompts');
const prompts = await response.json();
const container = document.getElementById('aiPromptsContent');
container.innerHTML = Object.entries(prompts).map(([key, prompt]) => `
<div class="card mb-4">
<div class="card-header bg-light">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1 fw-bold">${escapeHtml(prompt.name)}</h6>
<small class="text-muted">${escapeHtml(prompt.description)}</small>
</div>
<button class="btn btn-sm btn-outline-primary" onclick="copyPrompt('${key}')">
<i class="bi bi-clipboard me-1"></i>Kopier
</button>
</div>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-4">
<small class="text-muted">Model:</small>
<div><code>${escapeHtml(prompt.model)}</code></div>
</div>
<div class="col-md-4">
<small class="text-muted">Endpoint:</small>
<div><code>${escapeHtml(prompt.endpoint)}</code></div>
</div>
<div class="col-md-4">
<small class="text-muted">Parametre:</small>
<div><code>${JSON.stringify(prompt.parameters)}</code></div>
</div>
</div>
<div>
<small class="text-muted fw-bold d-block mb-2">System Prompt:</small>
<pre id="prompt_${key}" class="border rounded p-3 bg-light" style="max-height: 400px; overflow-y: auto; font-size: 0.85rem; white-space: pre-wrap;">${escapeHtml(prompt.prompt)}</pre>
</div>
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading AI prompts:', error);
document.getElementById('aiPromptsContent').innerHTML =
'<div class="alert alert-danger">Kunne ikke indlæse AI prompts</div>';
}
}
function copyPrompt(key) {
const promptElement = document.getElementById(`prompt_${key}`);
const text = promptElement.textContent;
navigator.clipboard.writeText(text).then(() => {
// Show success feedback
const btn = event.target.closest('button');
const originalHtml = btn.innerHTML;
btn.innerHTML = '<i class="bi bi-check me-1"></i>Kopieret!';
btn.classList.remove('btn-outline-primary');
btn.classList.add('btn-success');
setTimeout(() => {
btn.innerHTML = originalHtml;
btn.classList.remove('btn-success');
btn.classList.add('btn-outline-primary');
}, 2000);
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('da-DK', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
// Tab navigation
document.querySelectorAll('.settings-nav .nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const tab = link.dataset.tab;
// Update nav
document.querySelectorAll('.settings-nav .nav-link').forEach(l => l.classList.remove('active'));
link.classList.add('active');
// Update content
document.querySelectorAll('.tab-pane').forEach(pane => {
pane.classList.remove('show', 'active');
});
document.getElementById(tab).classList.add('show', 'active');
// Load data for tab
if (tab === 'users') {
loadUsers();
} else if (tab === 'ai-prompts') {
loadAIPrompts();
} else if (tab === 'modules') {
loadModules();
}
});
});
// Load modules function
async function loadModules() {
try {
const response = await fetch('/api/v1/modules');
const data = await response.json();
const modulesContainer = document.getElementById('activeModules');
if (!data.modules || Object.keys(data.modules).length === 0) {
modulesContainer.innerHTML = `
<div class="text-center py-4">
<i class="bi bi-inbox display-4 text-muted"></i>
<p class="text-muted mt-3 mb-0">Ingen aktive moduler fundet</p>
<small class="text-muted">Opret dit første modul med <code>python3 scripts/create_module.py</code></small>
</div>
`;
return;
}
const modulesList = Object.values(data.modules).map(module => `
<div class="card mb-2">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="mb-1 fw-bold">
${module.enabled ? '<i class="bi bi-check-circle-fill text-success me-2"></i>' : '<i class="bi bi-x-circle-fill text-danger me-2"></i>'}
${module.name}
<small class="text-muted fw-normal">v${module.version}</small>
</h6>
<p class="text-muted small mb-2">${module.description}</p>
<div class="d-flex gap-3 small">
<span><i class="bi bi-person me-1"></i>${module.author}</span>
<span><i class="bi bi-database me-1"></i>Prefix: <code>${module.table_prefix}</code></span>
${module.has_api ? '<span class="badge bg-primary">API</span>' : ''}
${module.has_frontend ? '<span class="badge bg-info">Frontend</span>' : ''}
</div>
</div>
<div>
<a href="${module.api_prefix}/health" target="_blank" class="btn btn-sm btn-outline-primary">
<i class="bi bi-heart-pulse me-1"></i>Health
</a>
</div>
</div>
</div>
</div>
`).join('');
modulesContainer.innerHTML = modulesList;
} catch (error) {
console.error('Error loading modules:', error);
document.getElementById('activeModules').innerHTML =
'<div class="alert alert-danger mb-0">Kunne ikke indlæse moduler</div>';
}
}
// 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();
}
});
}
// ====== SYNC MANAGEMENT ======
let syncLog = [];
async function loadSyncStats() {
try {
const response = await fetch('/api/v1/customers?limit=10000');
if (!response.ok) throw new Error('Failed to load customers');
const customers = await response.json();
const stats = {
total: customers.length,
withVtiger: customers.filter(c => c.vtiger_id).length,
withEconomic: customers.filter(c => c.economic_customer_number).length
};
document.getElementById('syncStatsCustomers').textContent = stats.total;
document.getElementById('syncStatsVtiger').textContent = stats.withVtiger;
document.getElementById('syncStatsEconomic').textContent = stats.withEconomic;
} catch (error) {
console.error('Error loading sync stats:', error);
}
}
async function loadSyncLog() {
const container = document.getElementById('syncLogContainer');
if (syncLog.length === 0) {
container.innerHTML = `
<div class="text-center py-5 text-muted">
<i class="bi bi-inbox" style="font-size: 2rem;"></i>
<p class="mt-2 mb-0">Ingen synkroniseringer endnu</p>
<small>Klik på en af sync knapperne ovenfor for at starte</small>
</div>
`;
return;
}
container.innerHTML = syncLog.map(log => `
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<div class="d-flex align-items-center mb-1">
<i class="bi ${log.status === 'success' ? 'bi-check-circle text-success' : log.status === 'error' ? 'bi-x-circle text-danger' : 'bi-info-circle text-primary'} me-2"></i>
<strong>${log.title}</strong>
</div>
<small class="text-muted">${log.message}</small>
</div>
<small class="text-muted">${new Date(log.timestamp).toLocaleString('da-DK')}</small>
</div>
</div>
`).join('');
}
function addSyncLogEntry(title, message, status = 'info') {
syncLog.unshift({
title,
message,
status,
timestamp: new Date().toISOString()
});
// Keep last 50 entries
if (syncLog.length > 50) {
syncLog = syncLog.slice(0, 50);
}
loadSyncLog();
}
async function syncFromVtiger() {
const btn = document.getElementById('btnSyncVtiger');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Synkroniserer...';
try {
addSyncLogEntry('vTiger Sync Startet', 'Henter firmaer fra vTiger...', 'info');
const response = await fetch('/api/v1/system/sync/vtiger', {
method: 'POST'
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Sync fejlede');
}
const result = await response.json();
const details = [
`Behandlet: ${result.total_processed || 0}`,
`Oprettet: ${result.created || 0}`,
`Opdateret: ${result.updated || 0}`,
`Sprunget over: ${result.skipped || 0}`
].join(' | ');
addSyncLogEntry(
'vTiger Sync Fuldført',
details,
'success'
);
document.getElementById('lastSyncVtiger').textContent = new Date().toLocaleString('da-DK');
await loadSyncStats();
showNotification('vTiger sync fuldført!', 'success');
} catch (error) {
addSyncLogEntry('vTiger Sync Fejl', error.message, 'error');
showNotification('Fejl: ' + error.message, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-download me-2"></i>Sync Firmaer fra vTiger';
}
}
async function syncVtigerContacts() {
const btn = document.getElementById('btnSyncVtigerContacts');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Synkroniserer...';
try {
addSyncLogEntry('vTiger Kontakt Sync Startet', 'Henter kontakter fra vTiger...', 'info');
const response = await fetch('/api/v1/system/sync/vtiger-contacts', {
method: 'POST'
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Sync fejlede');
}
const result = await response.json();
const details = [
`Behandlet: ${result.total_processed || 0}`,
`Oprettet: ${result.created || 0}`,
`Opdateret: ${result.updated || 0}`,
`Sprunget over: ${result.skipped || 0}`
].join(' | ');
addSyncLogEntry(
'vTiger Kontakt Sync Fuldført',
details,
'success'
);
showNotification('vTiger kontakt sync fuldført!', 'success');
} catch (error) {
addSyncLogEntry('vTiger Kontakt Sync Fejl', error.message, 'error');
showNotification('Fejl: ' + error.message, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-people me-2"></i>Sync Kontakter fra vTiger';
}
}
async function syncFromEconomic() {
const btn = document.getElementById('btnSyncEconomic');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Synkroniserer...';
try {
addSyncLogEntry('e-conomic Sync Startet', 'Henter kundenumre fra e-conomic...', 'info');
const response = await fetch('/api/v1/system/sync/economic', {
method: 'POST'
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Sync fejlede');
}
const result = await response.json();
const details = [
`Behandlet: ${result.total_processed || 0}`,
`Matchet: ${result.matched || 0}`,
`Ikke matchet: ${result.not_matched || 0}`
].join(' | ');
addSyncLogEntry(
'e-conomic Sync Fuldført',
details,
'success'
);
document.getElementById('lastSyncEconomic').textContent = new Date().toLocaleString('da-DK');
await loadSyncStats();
showNotification('e-conomic sync fuldført!', 'success');
} catch (error) {
addSyncLogEntry('e-conomic Sync Fejl', error.message, 'error');
showNotification('Fejl: ' + error.message, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-download me-2"></i>Sync Firmaer fra e-conomic';
}
}
async function syncCvrToEconomic() {
const btn = document.getElementById('btnSyncCvrEconomic');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Søger...';
try {
addSyncLogEntry('CVR Søgning Startet', 'Tjekker CVR numre i e-conomic...', 'info');
const response = await fetch('/api/v1/system/sync/cvr-to-economic', {
method: 'POST'
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Søgning fejlede');
}
const result = await response.json();
const details = [
`Kontrolleret: ${result.checked || 0}`,
`Fundet i e-conomic: ${result.found || 0}`,
`Linket: ${result.linked || 0}`
].join(' | ');
addSyncLogEntry(
'CVR Søgning Fuldført',
details,
'success'
);
await loadSyncStats();
showNotification('CVR søgning fuldført!', 'success');
} catch (error) {
addSyncLogEntry('CVR Søgning Fejl', error.message, 'error');
showNotification('Fejl: ' + error.message, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-search me-2"></i>Find Manglende CVR i e-conomic';
}
}
// Load sync data when sync tab is activated
const syncNavLink = document.querySelector('a[data-tab="sync"]');
if (syncNavLink) {
syncNavLink.addEventListener('click', () => {
loadSyncStats();
loadSyncLog();
});
}
// Notification helper
function showNotification(message, type = 'info') {
// Create toast notification
const toastContainer = document.getElementById('toastContainer') || createToastContainer();
const toastId = 'toast-' + Date.now();
const bgClass = type === 'success' ? 'bg-success' : type === 'error' ? 'bg-danger' : 'bg-info';
const icon = type === 'success' ? 'check-circle' : type === 'error' ? 'x-circle' : 'info-circle';
const toastHTML = `
<div id="${toastId}" class="toast align-items-center text-white ${bgClass} border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
<i class="bi bi-${icon} me-2"></i>${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHTML);
const toastElement = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastElement, { delay: 3000 });
toast.show();
// Remove from DOM after hiding
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
}
function createToastContainer() {
const container = document.createElement('div');
container.id = 'toastContainer';
container.className = 'toast-container position-fixed top-0 end-0 p-3';
container.style.zIndex = '9999';
document.body.appendChild(container);
return container;
}
// 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 %}