Files
foodlinkk-command-center/cockpit/templates/settings.html
T
Aissa c5ea8cbb63 Ship War Room UX, CEO inbox, mobile chat, and Agents NAS workspace.
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.
2026-08-16 22:40:32 +00:00

866 lines
38 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 &amp; 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 %}