docs: Create vTiger & Simply-CRM integration setup guide with credential requirements feat: Implement ticket system enhancements including relations, calendar events, templates, and AI suggestions refactor: Update ticket system migration to include audit logging and enhanced email metadata
883 lines
36 KiB
HTML
883 lines
36 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="#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>
|
|
|
|
<!-- 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>';
|
|
}
|
|
}
|
|
|
|
// Load on page ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadSettings();
|
|
loadUsers();
|
|
});
|
|
</script>
|
|
{% endblock %}
|