bmc_hub/app/settings/frontend/settings.html

601 lines
22 KiB
HTML
Raw Normal View History

{% 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="#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>
<!-- 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();
}
});
});
// Load on page ready
document.addEventListener('DOMContentLoaded', () => {
loadSettings();
loadUsers();
});
</script>
{% endblock %}