c5ea8cbb63
Make A2A togglable, keep reply context while reading, route agent data to //10.4.7.11/share/Agents, and polish themes plus export-intel sync cancel.
866 lines
38 KiB
HTML
866 lines
38 KiB
HTML
{% extends "base.html" %}
|
||
{% block content %}
|
||
<div x-data="settingsPage()" x-init="load()">
|
||
<header class="page-header settings-header">
|
||
<div>
|
||
<h1>Settings</h1>
|
||
<p class="subtitle page-tip">Configure integrations — Herman reads active settings from PostgreSQL.</p>
|
||
</div>
|
||
</header>
|
||
|
||
<nav class="settings-tabs">
|
||
<a href="/settings?tab=email" class="settings-tab {% if active_tab == 'email' %}active{% endif %}">Email</a>
|
||
<a href="/settings?tab=social" class="settings-tab {% if active_tab == 'social' %}active{% endif %}">Social API's</a>
|
||
<a href="/settings?tab=permissions" class="settings-tab {% if active_tab == 'permissions' %}active{% endif %}">Rechten Herman</a>
|
||
{% if can_manage_users %}
|
||
<a href="/settings?tab=users" class="settings-tab {% if active_tab == 'users' %}active{% endif %}">Gebruikers</a>
|
||
{% endif %}
|
||
<a href="/settings?tab=themes" class="settings-tab {% if active_tab == 'themes' %}active{% endif %}">Themes</a>
|
||
<a href="/settings?tab=general" class="settings-tab {% if active_tab == 'general' %}active{% endif %}">General</a>
|
||
</nav>
|
||
|
||
{% if active_tab == 'email' %}
|
||
<section class="panel settings-panel">
|
||
<div class="settings-panel-head">
|
||
<div>
|
||
<h2>Email accounts</h2>
|
||
<p class="page-tip">Kies welk mailbox Herman gebruikt om te versturen. IMAP velden zijn voor latere inbox-sync.</p>
|
||
</div>
|
||
<button type="button" class="btn btn-primary" @click="openForm()">+ Nieuw account</button>
|
||
</div>
|
||
|
||
<div class="settings-active-banner" x-show="activeAccount" x-cloak>
|
||
<span class="badge badge-positive">Actief</span>
|
||
<span x-text="activeAccount ? activeAccount.label + ' · ' + activeAccount.email_address : ''"></span>
|
||
</div>
|
||
|
||
<div class="email-account-list">
|
||
<template x-for="acc in accounts" :key="acc.id">
|
||
<article class="email-account-card" :class="{ 'is-active': acc.is_active }">
|
||
<div class="email-account-card-head">
|
||
<div>
|
||
<strong x-text="acc.label"></strong>
|
||
<span class="muted" x-text="acc.email_address"></span>
|
||
</div>
|
||
<span class="badge" :class="acc.is_active ? 'badge-positive' : 'badge-neutral'" x-text="acc.is_active ? 'Actief' : acc.provider"></span>
|
||
</div>
|
||
<div class="email-account-meta">
|
||
<span x-text="'SMTP: ' + (acc.smtp_host || '—') + ':' + (acc.smtp_port || '')"></span>
|
||
<span x-text="acc.smtp_password_set ? '🔒 wachtwoord opgeslagen' : '⚠️ geen SMTP wachtwoord'"></span>
|
||
<span x-show="acc.last_test_status" x-text="'Test: ' + acc.last_test_status"></span>
|
||
</div>
|
||
<div class="btn-group">
|
||
<button type="button" class="btn btn-secondary btn-sm" @click="edit(acc)">Bewerken</button>
|
||
<button type="button" class="btn btn-primary btn-sm" @click="activate(acc.id)" x-show="!acc.is_active">Maak actief</button>
|
||
<button type="button" class="btn btn-secondary btn-sm" @click="testAccount(acc.id)">Test SMTP</button>
|
||
<button type="button" class="btn btn-reject btn-sm" @click="remove(acc.id)">Verwijder</button>
|
||
</div>
|
||
</article>
|
||
</template>
|
||
<p class="empty-state" x-show="!accounts.length && !loading">Nog geen email account — voeg er een toe om Herman mail te laten versturen.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="modal" :class="{ open: formOpen }" @click.self="formOpen=false">
|
||
<div class="modal-card settings-modal">
|
||
<h3 x-text="form.id ? 'Email account bewerken' : 'Nieuw email account'"></h3>
|
||
|
||
<label class="form-label">Label
|
||
<input class="form-input" x-model="form.label" placeholder="bv. Foodlinkk sales" />
|
||
</label>
|
||
|
||
<label class="form-label">Email adres
|
||
<input class="form-input" type="email" x-model="form.email_address" placeholder="info@foodlinkk.nl" />
|
||
</label>
|
||
|
||
<label class="form-label">Provider (vult SMTP/IMAP automatisch in)
|
||
<select class="form-input" x-model="form.provider" @change="applyProvider()">
|
||
<option value="custom">Custom / eigen server</option>
|
||
<option value="gmail">Gmail</option>
|
||
<option value="outlook">Outlook / Microsoft 365</option>
|
||
<option value="zoho">Zoho Mail</option>
|
||
</select>
|
||
</label>
|
||
|
||
<div class="settings-form-grid">
|
||
<label class="form-label">SMTP host<input class="form-input" x-model="form.smtp_host" /></label>
|
||
<label class="form-label">SMTP port<input class="form-input" type="number" x-model.number="form.smtp_port" /></label>
|
||
<label class="form-label">SMTP user<input class="form-input" x-model="form.smtp_user" /></label>
|
||
<label class="form-label">SMTP wachtwoord
|
||
<input class="form-input" type="password" x-model="form.smtp_password" :placeholder="form.smtp_password_set ? '•••••••• (laat leeg om te behouden)' : 'App-wachtwoord'" />
|
||
</label>
|
||
</div>
|
||
|
||
<details class="settings-details">
|
||
<summary>IMAP (inbox sync — later)</summary>
|
||
<div class="settings-form-grid">
|
||
<label class="form-label">IMAP host<input class="form-input" x-model="form.imap_host" /></label>
|
||
<label class="form-label">IMAP port<input class="form-input" type="number" x-model.number="form.imap_port" /></label>
|
||
<label class="form-label">IMAP user<input class="form-input" x-model="form.imap_user" /></label>
|
||
<label class="form-label">IMAP wachtwoord
|
||
<input class="form-input" type="password" x-model="form.imap_password" :placeholder="form.imap_password_set ? '••••••••' : 'App-wachtwoord'" />
|
||
</label>
|
||
</div>
|
||
<label class="form-check">
|
||
<input type="checkbox" x-model="form.sync_enabled" /> Inbox sync inschakelen (wanneer beschikbaar)
|
||
</label>
|
||
</details>
|
||
|
||
<label class="form-check">
|
||
<input type="checkbox" x-model="form.is_active" /> Direct als actief account instellen
|
||
</label>
|
||
|
||
<div class="btn-group">
|
||
<button type="button" class="btn btn-primary" @click="save()" :disabled="saving">Opslaan</button>
|
||
<button type="button" class="btn btn-secondary" @click="testForm()" :disabled="saving">Test SMTP</button>
|
||
<button type="button" class="btn btn-secondary" @click="formOpen=false">Annuleren</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% elif active_tab == 'permissions' %}
|
||
<section class="panel settings-panel hm-neo" x-data="permissionsPage()" x-init="loadPerms()">
|
||
<div class="settings-panel-head">
|
||
<div>
|
||
<h2>Herman — Module rechten</h2>
|
||
<p class="page-tip">Bepaal welke bedrijfsprocessen Herman mag benaderen. Grant all = volledige toegang.</p>
|
||
</div>
|
||
<button type="button" class="btn btn-pulse-green" @click="grantAll()">👑 Alles aan Herman</button>
|
||
</div>
|
||
<p><span class="live-badge">Live</span> <span x-text="grantedCount + ' / ' + total + ' modules actief'"></span></p>
|
||
<div class="perm-grid">
|
||
<template x-for="p in perms" :key="p.module_key">
|
||
<div class="perm-card" :class="p.granted ? 'granted' : ''">
|
||
<div>
|
||
<strong x-text="p.module_label"></strong>
|
||
<p style="font-size:0.8rem;margin:0.25rem 0;color:#94a3b8" x-text="p.description"></p>
|
||
<small x-text="p.category"></small>
|
||
</div>
|
||
<label class="form-check">
|
||
<input type="checkbox" :checked="p.granted" @change="toggle(p.module_key, $event.target.checked)"> Toegang
|
||
</label>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</section>
|
||
|
||
{% elif active_tab == 'users' and can_manage_users %}
|
||
<section class="panel settings-panel hm-neo" x-data="usersAdminPage()" x-init="load()">
|
||
<div class="settings-panel-head">
|
||
<div>
|
||
<h2>Gebruikers & rechten</h2>
|
||
<p class="page-tip">Maak accounts aan en bepaal welke cockpit-pagina's zichtbaar zijn. Admins hebben altijd volledige toegang.</p>
|
||
</div>
|
||
<button type="button" class="btn btn-primary" @click="openCreate()">+ Nieuwe gebruiker</button>
|
||
</div>
|
||
|
||
<div class="email-account-list">
|
||
<template x-for="u in users" :key="u.id">
|
||
<article class="email-account-card" :class="{ 'is-active': u.is_active }">
|
||
<div class="email-account-card-head">
|
||
<div style="display:flex;align-items:center;gap:0.75rem;min-width:0">
|
||
<img class="user-card-avatar" x-show="u.avatar_url" :src="u.avatar_url" alt="" />
|
||
<span class="user-card-avatar fallback" x-show="!u.avatar_url" x-text="initials(u)"></span>
|
||
<div style="min-width:0">
|
||
<strong x-text="u.display_name"></strong>
|
||
<span class="muted" x-text="'@' + u.username"></span>
|
||
</div>
|
||
</div>
|
||
<span class="badge" :class="u.role === 'admin' ? 'badge-positive' : 'badge-neutral'" x-text="u.role"></span>
|
||
</div>
|
||
<div class="email-account-meta">
|
||
<span x-text="u.is_active ? 'Actief' : 'Uitgeschakeld'"></span>
|
||
<span x-text="u.role === 'admin' ? 'Alle pagina\'s' : ((u.permissions.pages||[]).length + ' pagina\'s')"></span>
|
||
<span x-show="u.last_login_at" x-text="'Laatste login: ' + (u.last_login_at || '')"></span>
|
||
</div>
|
||
<div class="btn-group">
|
||
<button type="button" class="btn btn-secondary btn-sm" @click="edit(u)">Bewerken</button>
|
||
<button type="button" class="btn btn-reject btn-sm" @click="remove(u)" x-show="u.id !== meId">Verwijder</button>
|
||
</div>
|
||
</article>
|
||
</template>
|
||
<p class="empty-state" x-show="!users.length && !loading">Nog geen gebruikers.</p>
|
||
</div>
|
||
|
||
<div class="modal" :class="{ open: formOpen }" @click.self="formOpen=false">
|
||
<div class="modal-card settings-modal" style="max-width:560px">
|
||
<h3 x-text="form.id ? 'Gebruiker bewerken' : 'Nieuwe gebruiker'"></h3>
|
||
|
||
<div class="user-form-avatar-row">
|
||
<img class="user-form-avatar-preview" x-show="form.avatar_url || form.avatarPreview"
|
||
:src="form.avatarPreview || form.avatar_url" alt="" />
|
||
<span class="user-form-avatar-preview fallback" x-show="!form.avatar_url && !form.avatarPreview"
|
||
x-text="formInitials()"></span>
|
||
<div>
|
||
<input type="file" accept="image/png,image/jpeg,image/webp,image/gif"
|
||
@change="onAvatarPick($event)" />
|
||
<p class="page-tip" style="margin:0.35rem 0 0">JPG/PNG/WEBP, max 3 MB. Bij nieuwe user: eerst opslaan, daarna foto.</p>
|
||
<button type="button" class="btn btn-sm btn-secondary" style="margin-top:0.35rem"
|
||
x-show="form.id && form.avatar_url" @click="removeAvatar()">Foto verwijderen</button>
|
||
</div>
|
||
</div>
|
||
|
||
<label class="form-label">Gebruikersnaam
|
||
<input class="form-input" x-model="form.username" :disabled="!!form.id" placeholder="bijv. sandra" autocomplete="off" />
|
||
</label>
|
||
<label class="form-label">Weergavenaam
|
||
<input class="form-input" x-model="form.display_name" placeholder="Sandra" />
|
||
</label>
|
||
<label class="form-label">Wachtwoord <span class="muted" x-show="form.id">(leeg = ongewijzigd)</span>
|
||
<input class="form-input" type="password" x-model="form.password" autocomplete="new-password" />
|
||
</label>
|
||
<label class="form-label">Rol
|
||
<select class="form-input" x-model="form.role">
|
||
<option value="user">User</option>
|
||
<option value="admin">Admin</option>
|
||
</select>
|
||
</label>
|
||
<label class="form-check" x-show="form.id">
|
||
<input type="checkbox" x-model="form.is_active" /> Account actief
|
||
</label>
|
||
<label class="form-check">
|
||
<input type="checkbox" x-model="form.manage_users" :disabled="form.role === 'admin'" /> Mag gebruikers beheren
|
||
</label>
|
||
|
||
<div x-show="form.role !== 'admin'" style="margin-top:0.75rem">
|
||
<strong style="font-size:0.85rem">Pagina-rechten</strong>
|
||
<div class="perm-grid" style="margin-top:0.5rem;grid-template-columns:1fr 1fr">
|
||
<template x-for="p in pages" :key="p.key">
|
||
<label class="form-check">
|
||
<input type="checkbox" :value="p.key" :checked="form.pages.includes(p.key)"
|
||
@change="togglePage(p.key, $event.target.checked)" />
|
||
<span x-text="p.label"></span>
|
||
</label>
|
||
</template>
|
||
</div>
|
||
<div class="btn-group" style="margin-top:0.5rem">
|
||
<button type="button" class="btn btn-sm btn-secondary" @click="selectAllPages()">Alles</button>
|
||
<button type="button" class="btn btn-sm btn-secondary" @click="form.pages=[]">Geen</button>
|
||
</div>
|
||
</div>
|
||
<p class="page-tip" x-show="form.role === 'admin'">Admins hebben automatisch toegang tot alle pagina's en gebruikersbeheer.</p>
|
||
|
||
<div class="btn-group" style="margin-top:1rem">
|
||
<button type="button" class="btn btn-primary" @click="save()" :disabled="saving" x-text="saving ? 'Bezig…' : 'Opslaan'"></button>
|
||
<button type="button" class="btn btn-secondary" @click="formOpen=false">Annuleer</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{% elif active_tab == 'social' %}
|
||
<section class="panel settings-panel" x-data="socialSettingsPage()" x-init="load()">
|
||
<div class="settings-panel-head">
|
||
<div>
|
||
<h2>Social API's</h2>
|
||
<p class="page-tip">Configureer later je API-sleutels per platform. Zonder keys worden posts in Automatisering overgeslagen (skip-modus).</p>
|
||
</div>
|
||
<a class="btn btn-sm btn-pulse" href="/marketing?tab=publish">→ Marketing Automatisering</a>
|
||
</div>
|
||
<div class="social-platform-grid">
|
||
<template x-for="p in platforms" :key="p.key">
|
||
<div class="perm-card" :class="p.configured ? 'granted' : ''">
|
||
<div class="social-platform-head">
|
||
<strong x-text="p.label"></strong>
|
||
<span class="badge" :class="p.is_active && p.configured ? 'badge-positive' : 'badge-muted'"
|
||
x-text="p.configured ? (p.is_active ? 'Actief' : 'Inactief') : 'Niet geconfigureerd'"></span>
|
||
</div>
|
||
<p class="page-tip" x-show="p.last_test_status" x-text="'Laatste test: ' + (p.last_test_status||'') + ' — ' + (p.last_test_message||'')"></p>
|
||
<template x-for="field in p.fields" :key="field.name">
|
||
<label class="form-label" x-text="field.label"></label>
|
||
<input class="form-input" :type="field.secret ? 'password' : 'text'"
|
||
:placeholder="field.placeholder"
|
||
x-model="p.form[field.name]" />
|
||
<small x-show="field.set && !p.form[field.name]" class="muted">Opgeslagen waarde blijft behouden</small>
|
||
</template>
|
||
<label class="form-check">
|
||
<input type="checkbox" x-model="p.form.is_active" /> Platform actief voor publicatie
|
||
</label>
|
||
<div class="btn-group">
|
||
<button type="button" class="btn btn-primary btn-sm" @click="save(p)" :disabled="p.saving">Opslaan</button>
|
||
<button type="button" class="btn btn-secondary btn-sm" @click="test(p)" :disabled="p.saving || !p.configured">Test</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</section>
|
||
|
||
{% elif active_tab == 'themes' %}
|
||
<section class="panel settings-panel" x-data="flkThemesPage()" x-init="init()">
|
||
<div class="settings-panel-head">
|
||
<div>
|
||
<h2>UI Themes</h2>
|
||
<p class="page-tip">UI-theme, pagina-achtergrond en sidebar-kleur zijn los van elkaar te kiezen.</p>
|
||
</div>
|
||
</div>
|
||
<div class="flk-theme-grid">
|
||
<template x-for="t in themes" :key="t.id">
|
||
<button type="button" class="flk-theme-card" :class="{ 'is-active': current === t.id }" @click="select(t.id)">
|
||
<div class="flk-theme-swatch" :style="swatch(t.id)"></div>
|
||
<strong x-text="t.name"></strong>
|
||
<small x-text="t.desc"></small>
|
||
</button>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="flk-theme-section">
|
||
<h3>Pagina achtergrond</h3>
|
||
<p class="page-tip">15 echte kleuren voor alle subpagina’s (niet zwart). Los van UI-theme en sidebar.</p>
|
||
<div class="flk-theme-grid">
|
||
<button type="button" class="flk-theme-card" :class="{ 'is-active': !bg }" @click="selectBg('')">
|
||
<div class="flk-theme-swatch" style="background:linear-gradient(135deg,#07131f,#123049)"></div>
|
||
<strong>Standaard</strong>
|
||
<small>Volgt UI-theme achtergrond</small>
|
||
</button>
|
||
<template x-for="b in backgrounds" :key="'bg-'+b.id">
|
||
<button type="button" class="flk-theme-card" :class="{ 'is-active': bg === b.id }" @click="selectBg(b.id)">
|
||
<div class="flk-theme-swatch" :style="bgSwatch(b.id)"></div>
|
||
<strong x-text="b.name"></strong>
|
||
<small x-text="b.desc"></small>
|
||
</button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flk-theme-section">
|
||
<h3>Sidebar Themes</h3>
|
||
<p class="page-tip">Los van het UI-theme — 10 diepe, strakke kleuren (geen rood/roze). Kies gradient of effen.</p>
|
||
<div class="flk-style-toggle" role="group" aria-label="Sidebar stijl">
|
||
<button type="button" :class="{ 'is-active': sidebarStyle === 'gradient' }" @click="selectSidebarStyle('gradient')">Gradient</button>
|
||
<button type="button" :class="{ 'is-active': sidebarStyle === 'plain' }" @click="selectSidebarStyle('plain')">Effen kleur</button>
|
||
</div>
|
||
<div class="flk-theme-grid">
|
||
<button type="button" class="flk-theme-card" :class="{ 'is-active': !sidebar }" @click="selectSidebar('')">
|
||
<div class="flk-theme-swatch" style="background:linear-gradient(180deg,#080c12,#121820)"></div>
|
||
<strong>Standaard</strong>
|
||
<small>Geen aparte sidebar-kleur (volgt UI)</small>
|
||
</button>
|
||
<template x-for="s in sidebars" :key="'sb-'+s.id">
|
||
<button type="button" class="flk-theme-card" :class="{ 'is-active': sidebar === s.id }" @click="selectSidebar(s.id)">
|
||
<div class="flk-theme-swatch" :data-sb="s.id" :data-sb-style="sidebarStyle" :style="sidebarSwatch(s.id)"></div>
|
||
<strong x-text="s.name"></strong>
|
||
<small x-text="s.desc"></small>
|
||
</button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{% elif active_tab == 'general' %}
|
||
<section class="panel settings-panel">
|
||
<h2>General</h2>
|
||
<p class="page-tip">Dashboard: <a href="http://10.4.7.18:8600">http://10.4.7.18:8600</a> · Telegram: @klaploper_bot · Ochtend briefing: 07:00</p>
|
||
<p class="page-tip">Meer instellingen (Telegram tijd, timezone) komen hier later.</p>
|
||
</section>
|
||
{% endif %}
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
function flkThemesPage() {
|
||
return {
|
||
themes: (window.FlkThemes && window.FlkThemes.list) || [],
|
||
sidebars: (window.FlkThemes && window.FlkThemes.sidebars) || [],
|
||
backgrounds: (window.FlkThemes && window.FlkThemes.backgrounds) || [],
|
||
current: 'ocean',
|
||
sidebar: '',
|
||
sidebarStyle: 'gradient',
|
||
bg: '',
|
||
init() {
|
||
this.themes = (window.FlkThemes && window.FlkThemes.list) || [];
|
||
this.sidebars = (window.FlkThemes && window.FlkThemes.sidebars) || [];
|
||
this.backgrounds = (window.FlkThemes && window.FlkThemes.backgrounds) || [];
|
||
this.current = (window.FlkThemes && window.FlkThemes.current()) || 'ocean';
|
||
this.sidebar = (window.FlkThemes && window.FlkThemes.currentSidebar()) || '';
|
||
this.sidebarStyle = (window.FlkThemes && window.FlkThemes.currentSidebarStyle && window.FlkThemes.currentSidebarStyle()) || 'gradient';
|
||
this.bg = (window.FlkThemes && window.FlkThemes.currentBg && window.FlkThemes.currentBg()) || '';
|
||
if (window.FlkThemes) {
|
||
window.FlkThemes.apply(this.current);
|
||
window.FlkThemes.applySidebar(this.sidebar);
|
||
if (window.FlkThemes.applySidebarStyle) window.FlkThemes.applySidebarStyle(this.sidebarStyle);
|
||
if (window.FlkThemes.applyBg) window.FlkThemes.applyBg(this.bg);
|
||
}
|
||
},
|
||
select(id) {
|
||
this.current = window.FlkThemes.apply(id);
|
||
if (window.Cockpit) Cockpit.toast('UI theme: ' + id, 'ok');
|
||
},
|
||
selectSidebar(id) {
|
||
this.sidebar = window.FlkThemes.applySidebar(id || '');
|
||
if (window.Cockpit) Cockpit.toast(id ? ('Sidebar: ' + id) : 'Sidebar: standaard', 'ok');
|
||
},
|
||
selectSidebarStyle(mode) {
|
||
this.sidebarStyle = window.FlkThemes.applySidebarStyle(mode || 'gradient');
|
||
if (window.Cockpit) Cockpit.toast(mode === 'plain' ? 'Sidebar: effen kleur' : 'Sidebar: gradient', 'ok');
|
||
},
|
||
selectBg(id) {
|
||
this.bg = window.FlkThemes.applyBg(id || '');
|
||
if (window.Cockpit) Cockpit.toast(id ? ('Achtergrond: ' + id) : 'Achtergrond: standaard', 'ok');
|
||
},
|
||
swatch(id) {
|
||
var map = {
|
||
ocean: 'linear-gradient(135deg,#07131f,#123049 40%,#22d3ee)',
|
||
aurora: 'linear-gradient(135deg,#0a1020,#1a2550 45%,#a78bfa 75%,#22d3ee)',
|
||
neon: 'linear-gradient(135deg,#050b14,#10243f 40%,#06b6d4 70%,#e879f9)',
|
||
glacier: 'linear-gradient(135deg,#0b1522,#1a2f4a 45%,#38bdf8 80%,#7dd3fc)',
|
||
cobalt: 'linear-gradient(135deg,#08101c,#172642 40%,#3b82f6 70%,#22d3ee)',
|
||
reef: 'linear-gradient(135deg,#04161a,#11353f 40%,#2dd4bf)',
|
||
plasma: 'linear-gradient(135deg,#0c0618,#26114a 40%,#c084fc 75%,#22d3ee)',
|
||
lagoon: 'linear-gradient(135deg,#031a1f,#0c3a45 40%,#22d3ee)',
|
||
indigo: 'linear-gradient(135deg,#07091a,#1a2250 40%,#818cf8 75%,#22d3ee)',
|
||
skyline: 'linear-gradient(135deg,#06101c,#142d4d 40%,#38bdf8)',
|
||
mintvolt: 'linear-gradient(135deg,#04140f,#10352a 40%,#34d399 70%,#22d3ee)',
|
||
sapphire: 'linear-gradient(135deg,#050d1f,#122650 40%,#3b82f6 70%,#38bdf8)',
|
||
arctic: 'linear-gradient(135deg,#0a121c,#1c2a3d 45%,#67e8f9)',
|
||
hyperion: 'linear-gradient(135deg,#041018,#0e2d42 40%,#22d3ee 70%,#a3e635)',
|
||
meridian: 'linear-gradient(135deg,#0a0716,#221645 40%,#c084fc 70%,#22d3ee)'
|
||
};
|
||
return 'background: ' + (map[id] || map.ocean) + ' !important;';
|
||
},
|
||
sidebarSwatch(id) {
|
||
var style = this.sidebarStyle || 'gradient';
|
||
var solids = {
|
||
aqua: '#0a4f5c',
|
||
azure: '#163a7a',
|
||
turquoise: '#0a453f',
|
||
cerulean: '#0c4a6e',
|
||
electric: '#0a3d4a',
|
||
violet: '#2a1f6b',
|
||
emerald: '#0a4a38',
|
||
midnight: '#0d1f3c',
|
||
steel: '#1a2d42',
|
||
nordic: '#0e3a4f',
|
||
};
|
||
var grads = {
|
||
aqua: 'linear-gradient(180deg, #062a32 0%, #0a4f5c 55%, #0d6b7a 100%)',
|
||
azure: 'linear-gradient(180deg, #0a1f45 0%, #163a7a 55%, #1e4f9e 100%)',
|
||
turquoise: 'linear-gradient(180deg, #052925 0%, #0a453f 55%, #0f5f56 100%)',
|
||
cerulean: 'linear-gradient(180deg, #06263a 0%, #0c4a6e 55%, #126087 100%)',
|
||
electric: 'linear-gradient(180deg, #041820 0%, #0a3d4a 50%, #0e5a6b 100%)',
|
||
violet: 'linear-gradient(180deg, #150e3a 0%, #2a1f6b 55%, #3b2f8a 100%)',
|
||
emerald: 'linear-gradient(180deg, #05291e 0%, #0a4a38 55%, #0f6349 100%)',
|
||
midnight: 'linear-gradient(180deg, #060e1c 0%, #0d1f3c 55%, #153056 100%)',
|
||
steel: 'linear-gradient(180deg, #0d1622 0%, #1a2d42 55%, #243b55 100%)',
|
||
nordic: 'linear-gradient(180deg, #071f2c 0%, #0e3a4f 55%, #15506a 100%)',
|
||
};
|
||
var bg = (style === 'plain')
|
||
? (solids[id] || solids.electric)
|
||
: (grads[id] || grads.electric);
|
||
return 'background: ' + bg + ' !important;';
|
||
},
|
||
bgSwatch(id) {
|
||
var map = {
|
||
navy: 'linear-gradient(135deg,#0d2b5c,#134078)',
|
||
royal: 'linear-gradient(135deg,#1a3a8f,#274bb8)',
|
||
azure: 'linear-gradient(135deg,#0a4f8a,#0d6cb8)',
|
||
sky: 'linear-gradient(135deg,#0e5f9e,#1880c9)',
|
||
cerulean: 'linear-gradient(135deg,#0c6a9e,#1490c8)',
|
||
teal: 'linear-gradient(135deg,#0c5c68,#128090)',
|
||
lagoon: 'linear-gradient(135deg,#0a6b7a,#0f8fa3)',
|
||
cyan: 'linear-gradient(135deg,#0a5568,#0e7a94)',
|
||
indigo: 'linear-gradient(135deg,#2f2a7a,#4338ca)',
|
||
violet: 'linear-gradient(135deg,#4a1d7a,#6d28d9)',
|
||
sapphire: 'linear-gradient(135deg,#123a8c,#1d4ed8)',
|
||
midnight: 'linear-gradient(135deg,#1a2f55,#274878)',
|
||
emerald: 'linear-gradient(135deg,#0d5c45,#059669)',
|
||
slate: 'linear-gradient(135deg,#243447,#334155)',
|
||
graphite: 'linear-gradient(135deg,#1e2a3a,#2c3b52)'
|
||
};
|
||
return 'background: ' + (map[id] || map.navy) + ' !important;';
|
||
}
|
||
};
|
||
}
|
||
</script>
|
||
|
||
<script>
|
||
function socialSettingsPage() {
|
||
const META = {
|
||
twitter: { label: 'X (Twitter)', fields: [
|
||
{ name: 'api_key', label: 'API Key', secret: true },
|
||
{ name: 'api_secret', label: 'API Secret', secret: true },
|
||
{ name: 'access_token', label: 'Access Token', secret: true },
|
||
{ name: 'access_secret', label: 'Access Secret', secret: true },
|
||
]},
|
||
linkedin: { label: 'LinkedIn', fields: [
|
||
{ name: 'access_token', label: 'Access Token', secret: true },
|
||
{ name: 'person_urn', label: 'Person URN', secret: false },
|
||
]},
|
||
instagram: { label: 'Instagram', fields: [
|
||
{ name: 'access_token', label: 'Access Token', secret: true },
|
||
{ name: 'page_id', label: 'Page / Business ID', secret: false },
|
||
]},
|
||
facebook: { label: 'Facebook', fields: [
|
||
{ name: 'access_token', label: 'Access Token', secret: true },
|
||
{ name: 'page_id', label: 'Page ID', secret: false },
|
||
]},
|
||
tiktok: { label: 'TikTok', fields: [
|
||
{ name: 'access_token', label: 'Access Token', secret: true },
|
||
{ name: 'open_id', label: 'Open ID', secret: false },
|
||
]},
|
||
pinterest: { label: 'Pinterest', fields: [
|
||
{ name: 'access_token', label: 'Access Token', secret: true },
|
||
{ name: 'board_id', label: 'Board ID', secret: false },
|
||
]},
|
||
};
|
||
|
||
return {
|
||
platforms: [],
|
||
async load() {
|
||
try {
|
||
const r = await Cockpit.api('/api/settings/social');
|
||
const items = r.items || [];
|
||
const byKey = {};
|
||
items.forEach(i => { byKey[i.platform] = i; });
|
||
this.platforms = (r.platforms || Object.keys(META)).map(key => {
|
||
const row = byKey[key] || {};
|
||
const cfg = row.config || {};
|
||
const meta = META[key] || { label: key, fields: [] };
|
||
const form = { is_active: !!row.is_active };
|
||
meta.fields.forEach(f => {
|
||
form[f.name] = '';
|
||
f.set = !!cfg[f.name + '_set'];
|
||
f.placeholder = f.set ? '••••••••' : '';
|
||
});
|
||
const configured = meta.fields.every(f => f.set || form[f.name]);
|
||
return {
|
||
key, label: meta.label, fields: meta.fields, form,
|
||
is_active: !!row.is_active,
|
||
configured: meta.fields.every(f => cfg[f.name + '_set']),
|
||
last_test_status: row.last_test_status,
|
||
last_test_message: row.last_test_message,
|
||
saving: false,
|
||
};
|
||
});
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
},
|
||
async save(p) {
|
||
p.saving = true;
|
||
try {
|
||
const body = { is_active: p.form.is_active };
|
||
p.fields.forEach(f => {
|
||
if (p.form[f.name]) body[f.name] = p.form[f.name];
|
||
});
|
||
await Cockpit.api('/api/settings/social/' + p.key, { method: 'PUT', body: JSON.stringify(body) });
|
||
Cockpit.toast(p.label + ' opgeslagen', 'success');
|
||
await this.load();
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
p.saving = false;
|
||
},
|
||
async test(p) {
|
||
p.saving = true;
|
||
try {
|
||
const r = await Cockpit.api('/api/settings/social/' + p.key + '/test', { method: 'POST' });
|
||
Cockpit.toast(r.message || (r.ok ? 'OK' : 'Mislukt'), r.ok ? 'success' : 'error');
|
||
await this.load();
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
p.saving = false;
|
||
},
|
||
};
|
||
}
|
||
|
||
|
||
function usersAdminPage() {
|
||
return {
|
||
loading: false,
|
||
saving: false,
|
||
users: [],
|
||
pages: [],
|
||
meId: null,
|
||
formOpen: false,
|
||
form: { id: null, username: '', display_name: '', password: '', role: 'user', is_active: true, manage_users: false, pages: [] },
|
||
async load() {
|
||
this.loading = true;
|
||
try {
|
||
const me = await Cockpit.api('/api/auth/me');
|
||
this.meId = me.user && me.user.id;
|
||
const r = await Cockpit.api('/api/settings/users');
|
||
this.users = r.users || [];
|
||
this.pages = r.pages || [];
|
||
} catch (e) {
|
||
Cockpit.toast((e && e.message) || 'Laden mislukt', 'error');
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
openCreate() {
|
||
this.form = { id: null, username: '', display_name: '', password: '', role: 'user', is_active: true, manage_users: false, pages: ['dashboard'], avatar_url: null, avatarPreview: null, avatarFile: null };
|
||
this.formOpen = true;
|
||
},
|
||
edit(u) {
|
||
this.form = {
|
||
id: u.id,
|
||
username: u.username,
|
||
display_name: u.display_name || '',
|
||
password: '',
|
||
role: u.role || 'user',
|
||
is_active: !!u.is_active,
|
||
manage_users: !!(u.permissions && u.permissions.manage_users),
|
||
pages: (u.permissions && u.permissions.pages) ? [...u.permissions.pages] : [],
|
||
avatar_url: u.avatar_url || null,
|
||
avatarPreview: null,
|
||
avatarFile: null,
|
||
};
|
||
this.formOpen = true;
|
||
},
|
||
initials(u) {
|
||
const n = ((u && (u.display_name || u.username)) || '?').trim();
|
||
const parts = n.split(/\s+/).filter(Boolean);
|
||
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
|
||
return n.slice(0, 2).toUpperCase();
|
||
},
|
||
formInitials() {
|
||
return this.initials({ display_name: this.form.display_name, username: this.form.username });
|
||
},
|
||
onAvatarPick(ev) {
|
||
const file = ev.target.files && ev.target.files[0];
|
||
if (!file) return;
|
||
this.form.avatarFile = file;
|
||
this.form.avatarPreview = URL.createObjectURL(file);
|
||
},
|
||
async uploadAvatarFor(userId, file) {
|
||
const fd = new FormData();
|
||
fd.append('file', file);
|
||
const res = await fetch('/api/settings/users/' + userId + '/avatar', { method: 'POST', body: fd, credentials: 'same-origin' });
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) throw new Error(data.detail || 'Foto upload mislukt');
|
||
return data.user;
|
||
},
|
||
async removeAvatar() {
|
||
if (!this.form.id) return;
|
||
if (!confirm('Profielfoto verwijderen?')) return;
|
||
try {
|
||
await Cockpit.api('/api/settings/users/' + this.form.id + '/avatar', { method: 'DELETE' });
|
||
this.form.avatar_url = null;
|
||
this.form.avatarPreview = null;
|
||
this.form.avatarFile = null;
|
||
Cockpit.toast('Foto verwijderd', 'success');
|
||
await this.load();
|
||
} catch (e) {
|
||
Cockpit.toast((e && e.message) || 'Mislukt', 'error');
|
||
}
|
||
},
|
||
togglePage(key, on) {
|
||
if (on && !this.form.pages.includes(key)) this.form.pages.push(key);
|
||
if (!on) this.form.pages = this.form.pages.filter(k => k !== key);
|
||
},
|
||
selectAllPages() { this.form.pages = this.pages.map(p => p.key); },
|
||
async save() {
|
||
this.saving = true;
|
||
try {
|
||
const permissions = {
|
||
pages: this.form.role === 'admin' ? this.pages.map(p => p.key) : this.form.pages,
|
||
manage_users: this.form.role === 'admin' ? true : !!this.form.manage_users,
|
||
};
|
||
let saved = null;
|
||
if (this.form.id) {
|
||
const body = {
|
||
display_name: this.form.display_name,
|
||
role: this.form.role,
|
||
is_active: this.form.is_active,
|
||
permissions,
|
||
};
|
||
if (this.form.password) body.password = this.form.password;
|
||
saved = await Cockpit.api('/api/settings/users/' + this.form.id, { method: 'PUT', body: JSON.stringify(body) });
|
||
saved = saved.user || saved;
|
||
} else {
|
||
if (!this.form.username || !this.form.password) {
|
||
Cockpit.toast('Gebruikersnaam en wachtwoord verplicht', 'error');
|
||
return;
|
||
}
|
||
saved = await Cockpit.api('/api/settings/users', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
username: this.form.username,
|
||
password: this.form.password,
|
||
display_name: this.form.display_name || this.form.username,
|
||
role: this.form.role,
|
||
permissions,
|
||
}),
|
||
});
|
||
saved = saved.user || saved;
|
||
}
|
||
if (this.form.avatarFile && saved && saved.id) {
|
||
await this.uploadAvatarFor(saved.id, this.form.avatarFile);
|
||
}
|
||
this.formOpen = false;
|
||
Cockpit.toast('Gebruiker opgeslagen', 'success');
|
||
await this.load();
|
||
} catch (e) {
|
||
Cockpit.toast((e && e.message) || 'Opslaan mislukt', 'error');
|
||
} finally {
|
||
this.saving = false;
|
||
}
|
||
},
|
||
async remove(u) {
|
||
if (!confirm('Verwijder gebruiker ' + u.username + '?')) return;
|
||
try {
|
||
await Cockpit.api('/api/settings/users/' + u.id, { method: 'DELETE' });
|
||
Cockpit.toast('Verwijderd', 'success');
|
||
await this.load();
|
||
} catch (e) {
|
||
Cockpit.toast((e && e.message) || 'Verwijderen mislukt', 'error');
|
||
}
|
||
},
|
||
};
|
||
}
|
||
|
||
function permissionsPage() {
|
||
return {
|
||
perms: [], grantedCount: 0, total: 0,
|
||
async loadPerms() {
|
||
const r = await Cockpit.api('/api/settings/permissions');
|
||
this.perms = r.items || [];
|
||
this.grantedCount = r.granted_count || 0;
|
||
this.total = r.total || 0;
|
||
},
|
||
async toggle(key, granted) {
|
||
await Cockpit.api('/api/settings/permissions/' + key, { method: 'PUT', body: JSON.stringify({ granted }) });
|
||
await this.loadPerms();
|
||
},
|
||
async grantAll() {
|
||
const r = await Cockpit.api('/api/settings/permissions/grant-all', { method: 'POST' });
|
||
Cockpit.toast(r.message || 'Alle rechten toegekend', 'success');
|
||
await this.loadPerms();
|
||
}
|
||
};
|
||
}
|
||
</script>
|
||
{% if active_tab == 'email' %}
|
||
<script>
|
||
const PROVIDERS = {
|
||
gmail: { smtp_host: 'smtp.gmail.com', smtp_port: 587, imap_host: 'imap.gmail.com', imap_port: 993 },
|
||
outlook: { smtp_host: 'smtp.office365.com', smtp_port: 587, imap_host: 'outlook.office365.com', imap_port: 993 },
|
||
zoho: { smtp_host: 'smtp.zoho.eu', smtp_port: 587, imap_host: 'imap.zoho.eu', imap_port: 993 },
|
||
custom: { smtp_host: '', smtp_port: 587, imap_host: '', imap_port: 993 },
|
||
};
|
||
|
||
function settingsPage() {
|
||
return {
|
||
accounts: [],
|
||
activeAccount: null,
|
||
loading: true,
|
||
formOpen: false,
|
||
saving: false,
|
||
form: defaultForm(),
|
||
|
||
async load() {
|
||
this.loading = true;
|
||
try {
|
||
const r = await Cockpit.api('/api/settings/email');
|
||
this.accounts = r.accounts || [];
|
||
this.activeAccount = this.accounts.find(a => a.is_active) || null;
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
this.loading = false;
|
||
},
|
||
|
||
defaultForm() { return defaultForm(); },
|
||
|
||
openForm() {
|
||
this.form = defaultForm();
|
||
this.formOpen = true;
|
||
},
|
||
|
||
edit(acc) {
|
||
this.form = Object.assign(defaultForm(), acc, { smtp_password: '', imap_password: '' });
|
||
this.formOpen = true;
|
||
},
|
||
|
||
applyProvider() {
|
||
const p = PROVIDERS[this.form.provider] || PROVIDERS.custom;
|
||
this.form.smtp_host = p.smtp_host;
|
||
this.form.smtp_port = p.smtp_port;
|
||
this.form.imap_host = p.imap_host;
|
||
this.form.imap_port = p.imap_port;
|
||
if (this.form.email_address && !this.form.smtp_user) {
|
||
this.form.smtp_user = this.form.email_address;
|
||
this.form.imap_user = this.form.email_address;
|
||
}
|
||
},
|
||
|
||
bodyFromForm() {
|
||
const b = {
|
||
label: this.form.label,
|
||
email_address: this.form.email_address,
|
||
provider: this.form.provider,
|
||
is_active: this.form.is_active,
|
||
smtp_host: this.form.smtp_host,
|
||
smtp_port: this.form.smtp_port,
|
||
smtp_user: this.form.smtp_user,
|
||
imap_host: this.form.imap_host,
|
||
imap_port: this.form.imap_port,
|
||
imap_user: this.form.imap_user,
|
||
sync_enabled: this.form.sync_enabled,
|
||
};
|
||
if (this.form.smtp_password) b.smtp_password = this.form.smtp_password;
|
||
if (this.form.imap_password) b.imap_password = this.form.imap_password;
|
||
return b;
|
||
},
|
||
|
||
async save() {
|
||
this.saving = true;
|
||
try {
|
||
const body = this.bodyFromForm();
|
||
if (this.form.id) {
|
||
await Cockpit.api('/api/settings/email/' + this.form.id, { method: 'PUT', body: JSON.stringify(body) });
|
||
} else {
|
||
await Cockpit.api('/api/settings/email', { method: 'POST', body: JSON.stringify(body) });
|
||
}
|
||
Cockpit.toast('Opgeslagen', 'success');
|
||
this.formOpen = false;
|
||
await this.load();
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
this.saving = false;
|
||
},
|
||
|
||
async activate(id) {
|
||
try {
|
||
await Cockpit.api('/api/settings/email/' + id + '/activate', { method: 'POST' });
|
||
Cockpit.toast('Actief account gewijzigd', 'success');
|
||
await this.load();
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
},
|
||
|
||
async testAccount(id) {
|
||
try {
|
||
Cockpit.toast('SMTP test…', 'info');
|
||
const r = await Cockpit.api('/api/settings/email/' + id + '/test', { method: 'POST' });
|
||
Cockpit.toast(r.message || 'SMTP OK', r.ok ? 'success' : 'error');
|
||
await this.load();
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
},
|
||
|
||
async testForm() {
|
||
this.saving = true;
|
||
try {
|
||
const r = await Cockpit.api('/api/settings/email/test', {
|
||
method: 'POST',
|
||
body: JSON.stringify(this.bodyFromForm()),
|
||
});
|
||
Cockpit.toast(r.message || 'SMTP OK', r.ok ? 'success' : 'error');
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
this.saving = false;
|
||
},
|
||
|
||
async remove(id) {
|
||
if (!confirm('Email account verwijderen?')) return;
|
||
try {
|
||
await Cockpit.api('/api/settings/email/' + id, { method: 'DELETE' });
|
||
Cockpit.toast('Verwijderd', 'success');
|
||
await this.load();
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
},
|
||
};
|
||
}
|
||
|
||
function defaultForm() {
|
||
return {
|
||
id: null, label: '', email_address: '', provider: 'gmail', is_active: true,
|
||
smtp_host: 'smtp.gmail.com', smtp_port: 587, smtp_user: '', smtp_password: '', smtp_password_set: false,
|
||
imap_host: 'imap.gmail.com', imap_port: 993, imap_user: '', imap_password: '', imap_password_set: false,
|
||
sync_enabled: false,
|
||
};
|
||
}
|
||
</script>
|
||
{% endif %}
|
||
{% endblock %}
|