SysOps: approved_backup — 2026-08-16 11:01 UTC
This commit is contained in:
@@ -2,6 +2,48 @@
|
||||
{% block extra_head %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=13" />
|
||||
<style>
|
||||
.reclame-kpi-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.65rem;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.reclame-kpi-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
button.reclame-kpi-btn.kpi-card {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background: var(--bg-elevated, #0f172a);
|
||||
border: 1px solid rgba(56,189,248,0.55);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
button.reclame-kpi-btn.kpi-card:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 22px rgba(56,189,248,0.18);
|
||||
border-color: rgba(56,189,248,0.9);
|
||||
}
|
||||
button.reclame-kpi-btn.kpi-card:focus-visible {
|
||||
outline: 2px solid rgba(56,189,248,0.75);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
button.reclame-kpi-btn.kpi-card.is-active {
|
||||
border-color: #38bdf8;
|
||||
background: rgba(56,189,248,0.08);
|
||||
box-shadow: 0 0 0 1px rgba(56,189,248,0.25);
|
||||
}
|
||||
button.reclame-kpi-btn.kpi-card span { color: var(--text-secondary, #94a3b8); font-size: 0.85rem; }
|
||||
button.reclame-kpi-btn.kpi-card strong { color: var(--text-primary, #f8fafc); font-size: 1.15rem; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div x-data="marketingHub()" x-init="init()">
|
||||
@@ -133,17 +175,23 @@
|
||||
<p class="hint">Live folders van <a href="https://www.reclamefolder.nl/categorieen/supermarkt/" target="_blank" rel="noopener">Reclamefolder.nl</a> — klik om de folder te openen.</p>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.5rem;flex-wrap:wrap">
|
||||
<button class="btn btn-sm btn-pulse-green" @click="refreshReclamefolders()" :disabled="busy">↻ Ophalen van Reclamefolder.nl</button>
|
||||
<button type="button" class="btn btn-sm btn-pulse-green" @click="refreshReclamefolders()" :disabled="reclameBusy" x-text="reclameBusy ? '⏳ Bezig met ophalen…' : '↻ Ophalen van Reclamefolder.nl'"></button>
|
||||
<a class="btn btn-sm" href="https://www.reclamefolder.nl/categorieen/supermarkt/" target="_blank" rel="noopener">Bron site →</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="reclame-kpi-row kpi-row" style="margin-bottom:1rem">
|
||||
<div class="kpi-card"><span>Ketens</span><strong x-text="reclameStats.chains||0"></strong></div>
|
||||
<div class="kpi-card kpi-green"><span>Folders</span><strong x-text="filteredPromos.length"></strong></div>
|
||||
<div class="kpi-card"><span>Totaal geladen</span><strong x-text="promos.length"></strong></div>
|
||||
<div class="reclame-kpi-row kpi-row" style="margin-bottom:1rem" role="group" aria-label="Reclame snelfilters">
|
||||
<button type="button" class="kpi-card clickable-kpi reclame-kpi-btn" :class="{ 'is-active': reclameKpi === 'ketens' }" @click="onReclameKpi('ketens')" title="Open ketens-filter">
|
||||
<span>Ketens</span><strong x-text="reclameStats.chains||0"></strong>
|
||||
</button>
|
||||
<button type="button" class="kpi-card kpi-green clickable-kpi reclame-kpi-btn" :class="{ 'is-active': reclameKpi === 'folders' }" @click="onReclameKpi('folders')" title="Toon folders">
|
||||
<span>Folders</span><strong x-text="filteredPromos.length"></strong>
|
||||
</button>
|
||||
<button type="button" class="kpi-card clickable-kpi reclame-kpi-btn" :class="{ 'is-active': reclameKpi === 'totaal' }" @click="onReclameKpi('totaal')" title="Toon alles / filters wissen">
|
||||
<span>Totaal geladen</span><strong x-text="promos.length"></strong>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-grid reclame-filters" style="margin-bottom:1rem">
|
||||
<select class="form-input" x-model="reclameFilter.chain" @change="loadPromos()">
|
||||
<select class="form-input" x-ref="chainFilter" x-model="reclameFilter.chain" @change="loadPromos()">
|
||||
<option value="">Alle ketens</option>
|
||||
<template x-for="c in reclameChains" :key="c">
|
||||
<option :value="c" x-text="c"></option>
|
||||
@@ -177,7 +225,7 @@
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
<p x-show="!filteredPromos.length && !busy" class="muted">Geen folders — pas filters aan of klik «Ophalen van Reclamefolder.nl».</p>
|
||||
<p x-show="!filteredPromos.length && !reclameBusy" class="muted">Geen folders — pas filters aan of klik «Ophalen van Reclamefolder.nl».</p>
|
||||
<h3 style="margin-top:1.5rem">Nieuwe folder registreren</h3>
|
||||
<form class="form-grid" @submit.prevent="addPromo">
|
||||
<input class="form-input" x-model="promoForm.chain" placeholder="Keten (AH, Jumbo…)" />
|
||||
@@ -397,10 +445,11 @@ function marketingHub() {
|
||||
return {
|
||||
tab: new URLSearchParams(location.search).get('tab') || 'live',
|
||||
tabIndicatorTop: 0, tabIndicatorH: 44,
|
||||
busy: false, dashboard: {}, socialAnalytics: '',
|
||||
busy: false, reclameBusy: false, dashboard: {}, socialAnalytics: '',
|
||||
regRows: { regelgeving: [], cbs: [], markt: [] },
|
||||
promos: [], bookmarks: [], bookmarkIds: {},
|
||||
reclameChains: [], reclameFilter: { chain: '', q: '', folder_type: '', valid_days: '' },
|
||||
reclameKpi: 'folders',
|
||||
reclameStats: { chains: 0 },
|
||||
publishChannels: [], publishForm: { text: '', image_url: '', media_ids: [], channels: [], project_id: '' },
|
||||
publishHistory: [], publishJobId: null, publishJobStatus: '', publishBusy: false,
|
||||
@@ -457,7 +506,30 @@ function marketingHub() {
|
||||
this.tabIndicatorTop = btn.offsetTop - nav.scrollTop;
|
||||
this.tabIndicatorH = btn.offsetHeight;
|
||||
},
|
||||
onReclameKpi(which) {
|
||||
this.reclameKpi = which;
|
||||
if (which === 'ketens') {
|
||||
this.$nextTick(() => {
|
||||
const el = this.$refs.chainFilter;
|
||||
if (el) {
|
||||
el.focus();
|
||||
try { el.showPicker && el.showPicker(); } catch (e) {}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (which === 'folders') {
|
||||
this.reclameFilter = { chain: '', q: '', folder_type: 'folder', valid_days: '' };
|
||||
this.loadPromos();
|
||||
this.$nextTick(() => document.querySelector('.promo-grid')?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
|
||||
return;
|
||||
}
|
||||
this.reclameFilter = { chain: '', q: '', folder_type: '', valid_days: '' };
|
||||
this.loadPromos();
|
||||
this.$nextTick(() => document.querySelector('.promo-grid')?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
|
||||
},
|
||||
async loadReclameChains() {
|
||||
|
||||
try {
|
||||
const d = await fetch('/api/retail/reclamefolder/chains').then(r => r.json());
|
||||
this.reclameChains = d.chains || [];
|
||||
@@ -471,10 +543,15 @@ function marketingHub() {
|
||||
if (this.reclameFilter.q) params.set('q', this.reclameFilter.q);
|
||||
if (this.reclameFilter.folder_type) params.set('folder_type', this.reclameFilter.folder_type);
|
||||
if (this.reclameFilter.valid_days) params.set('valid_days', this.reclameFilter.valid_days);
|
||||
let items = (await fetch('/api/retail/promo-campaigns?' + params).then(r => r.json())).items || [];
|
||||
const resp = await fetch('/api/retail/promo-campaigns?' + params);
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
let items = (await resp.json()).items || [];
|
||||
items = items.filter(p => p.source === 'reclamefolder.nl' || (p.folder_path || '').startsWith('http') || (p.metadata && (typeof p.metadata === 'object' ? p.metadata.edition_id : String(p.metadata).includes('edition_id'))));
|
||||
this.promos = items;
|
||||
if (!items.length && !this.reclameFilter.q && !this.reclameFilter.chain) await this.refreshReclamefolders(false);
|
||||
// Alleen auto-sync bij eerste lege load, niet tijdens handmatige sync (voorkomt nesting)
|
||||
if (!items.length && !this.reclameBusy && !this.reclameFilter.q && !this.reclameFilter.chain && !this.reclameFilter.folder_type && !this.reclameFilter.valid_days) {
|
||||
await this.refreshReclamefolders(false);
|
||||
}
|
||||
} catch (e) { this.promos = []; }
|
||||
},
|
||||
promoUrl(p) {
|
||||
@@ -489,15 +566,26 @@ function marketingHub() {
|
||||
return 'https://www.reclamefolder.nl/categorieen/supermarkt/';
|
||||
},
|
||||
async refreshReclamefolders(toast = true) {
|
||||
this.busy = true;
|
||||
if (this.reclameBusy) return;
|
||||
this.reclameBusy = true;
|
||||
try {
|
||||
const d = await fetch('/api/retail/reclamefolder/refresh', { method: 'POST' }).then(r => r.json());
|
||||
const resp = await fetch('/api/retail/reclamefolder/refresh', {
|
||||
method: 'POST',
|
||||
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
|
||||
body: '{}',
|
||||
});
|
||||
const d = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || d.ok === false) {
|
||||
throw new Error(d.detail || d.error || ('HTTP ' + resp.status));
|
||||
}
|
||||
await this.loadPromos();
|
||||
await this.loadReclameChains();
|
||||
if (toast) Cockpit.toast((d.synced || 0) + ' folders opgehaald van Reclamefolder.nl', 'success');
|
||||
if (typeof d.synced === 'number') this.reclameStats.synced = d.synced;
|
||||
if (toast) Cockpit.toast((d.synced || this.promos.length || 0) + ' folders opgehaald van Reclamefolder.nl', 'success');
|
||||
} catch (e) {
|
||||
if (toast) Cockpit.toast('Reclamefolder ophalen mislukt', 'error');
|
||||
} finally { this.busy = false; }
|
||||
console.error('reclamefolder refresh', e);
|
||||
if (toast) Cockpit.toast('Reclamefolder ophalen mislukt: ' + (e.message || e), 'error');
|
||||
} finally { this.reclameBusy = false; }
|
||||
},
|
||||
async addPromo() {
|
||||
await fetch('/api/retail/promo-campaigns', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.promoForm) });
|
||||
@@ -545,13 +633,18 @@ function marketingHub() {
|
||||
},
|
||||
async refreshRss() { this.busy=true; try{ await fetch('/api/retail/rss/refresh',{method:'POST'}); await this.loadDashboard(); }finally{ this.busy=false; } },
|
||||
async refreshAll(toast) {
|
||||
this.busy = true;
|
||||
const silent = toast === false;
|
||||
if (!silent) this.busy = true;
|
||||
try {
|
||||
await fetch('/api/retail/rss/refresh', { method: 'POST' });
|
||||
await this.loadDashboard();
|
||||
if (this.tab === 'reclame' && !this.reclameBusy) {
|
||||
await this.loadPromos();
|
||||
await this.loadReclameChains();
|
||||
}
|
||||
this.renderViz(window.__vizMode || 'neo-bars');
|
||||
if (toast !== false) Cockpit.toast('Marketing data bijgewerkt', 'success');
|
||||
} finally { this.busy = false; }
|
||||
if (!silent) Cockpit.toast('Marketing data bijgewerkt', 'success');
|
||||
} finally { if (!silent) this.busy = false; }
|
||||
},
|
||||
async addMention() { await Cockpit.api('/mentions',{method:'POST',body:JSON.stringify(this.mentionForm)}); location.reload(); },
|
||||
async deleteMention(id) { if(!Cockpit.confirmDelete()) return; await Cockpit.api('/mentions/'+id,{method:'DELETE'}); location.reload(); },
|
||||
|
||||
Reference in New Issue
Block a user