bmc_hub/app/modules/website_content/templates/index.html

128 lines
14 KiB
HTML
Raw Normal View History

2026-08-28 20:49:55 +02:00
{% extends "shared/frontend/base.html" %}
{% block title %}Website-indhold · BMC Hub{% endblock %}
{% block extra_css %}
<style>
.wc-shell { max-width: 1500px; margin: 0 auto; }
.wc-hero { background: linear-gradient(135deg, #06364b, #087894); color: #fff; border-radius: 22px; padding: 1.5rem; box-shadow: 0 14px 35px rgba(5,54,75,.2); }
.wc-kicker { text-transform: uppercase; letter-spacing: .13em; font-size: .72rem; opacity: .74; font-weight: 700; }
.wc-panel { background: var(--card-bg, #fff); color: var(--text-color, #17202a); border: 1px solid var(--border-color, #dde4e8); border-radius: 18px; overflow: hidden; }
.wc-panel-head { padding: 1rem 1.15rem; border-bottom: 1px solid var(--border-color, #dde4e8); display:flex; align-items:center; justify-content:space-between; gap:1rem; }
.wc-list { display:grid; gap:.75rem; padding:1rem; }
.wc-row { border:1px solid var(--border-color, #dde4e8); border-radius:14px; padding:1rem; display:grid; grid-template-columns:minmax(0,1fr) auto; gap:1rem; align-items:center; background:var(--body-bg, #fff); }
.wc-logo { width:92px; height:54px; object-fit:contain; border-radius:8px; padding:5px; background:#fff; border:1px solid #e5e7eb; }
.wc-meta { color:var(--text-muted, #68737d); font-size:.84rem; }
.wc-severity { display:inline-flex; align-items:center; gap:.35rem; border-radius:999px; padding:.25rem .6rem; font-size:.75rem; font-weight:700; }
.wc-severity.ok { color:#087b4d; background:#d9f8ea; } .wc-severity.info { color:#1261a0; background:#ddebfa; }
.wc-severity.warning { color:#926300; background:#fff0c2; } .wc-severity.critical { color:#b42318; background:#fee4e2; }
.wc-hidden { opacity:.55; }
.wc-empty { text-align:center; color:var(--text-muted, #68737d); padding:2.5rem 1rem; }
.wc-tabs { display:flex; gap:.5rem; overflow:auto; padding-bottom:.25rem; }
.wc-tabs button { white-space:nowrap; border-radius:999px; }
.form-control, .form-select { background-color:var(--card-bg, #fff); color:var(--text-color, #17202a); border-color:var(--border-color, #ced4da); }
@media(max-width:700px) { .wc-row { grid-template-columns:1fr; } .wc-actions { width:100%; display:flex; } .wc-actions .btn { flex:1; } }
</style>
{% endblock %}
{% block content %}
{% set can_edit = current_user.is_superadmin or 'website_content.edit' in (current_user.permissions or []) %}
<main class="container-fluid py-4 px-lg-4 wc-shell" data-can-edit="{{ 'true' if can_edit else 'false' }}">
<section class="wc-hero mb-4 d-flex flex-wrap justify-content-between align-items-end gap-3">
<div><div class="wc-kicker mb-2">Nordic Top · website administration</div><h1 class="h3 mb-2">Website-indhold</h1><p class="mb-0 opacity-75">Administrér kundelogoer, aktuel driftsstatus og offentlig historik.</p></div>
<div id="connectionState" class="small opacity-75"><i class="bi bi-circle-fill me-1"></i> Forbinder…</div>
</section>
<div class="wc-tabs mb-3" role="tablist">
<button class="btn btn-primary" data-tab="customers"><i class="bi bi-buildings me-1"></i>Kundereferencer</button>
<button class="btn btn-outline-secondary" data-tab="operations"><i class="bi bi-broadcast me-1"></i>Aktuel drift</button>
<button class="btn btn-outline-secondary" data-tab="incidents"><i class="bi bi-clock-history me-1"></i>Historik</button>
</div>
<section class="wc-panel" id="contentPanel">
<div class="wc-panel-head">
<div><h2 class="h5 mb-1" id="panelTitle">Kundereferencer</h2><div class="wc-meta" id="panelSubtitle">Rækkefølge og logoer på forsiden</div></div>
{% if can_edit %}<button class="btn btn-primary" id="createBtn"><i class="bi bi-plus-lg me-1"></i>Opret</button>{% endif %}
</div>
<div class="wc-list" id="contentList"><div class="wc-empty"><div class="spinner-border spinner-border-sm me-2"></div>Indlæser…</div></div>
</section>
</main>
<div class="modal fade" id="editorModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable"><form class="modal-content" id="editorForm">
<div class="modal-header"><h2 class="modal-title fs-5" id="editorTitle">Redigér</h2><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body" id="editorFields"></div>
<div class="modal-footer"><button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Annuller</button><button class="btn btn-primary" type="submit">Gem</button></div>
</form></div>
</div>
<div class="toast-container position-fixed bottom-0 end-0 p-3"><div id="wcToast" class="toast"><div class="toast-body" id="wcToastText"></div></div></div>
{% endblock %}
{% block extra_js %}
<script>
(() => {
const api = '/api/v1/website-content';
const canEdit = document.querySelector('.wc-shell').dataset.canEdit === 'true';
const labels = {
customers: ['Kundereferencer', 'Rækkefølge og logoer på forsiden'],
operations: ['Aktuel driftsstatus', 'Aktive, kommende og skjulte meddelelser'],
incidents: ['Driftshistorik', 'Offentlige og skjulte afsluttede hændelser']
};
let tab = 'customers', items = [], editing = null;
const modal = new bootstrap.Modal(document.getElementById('editorModal'));
const esc = value => String(value ?? '').replace(/[&<>'"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]));
const localInput = value => value ? String(value).replace(' ', 'T').slice(0,16) : '';
const formatDate = value => value ? new Intl.DateTimeFormat('da-DK', {dateStyle:'medium', timeStyle:'short'}).format(new Date(String(value).replace(' ', 'T'))) : 'Ikke angivet';
const toast = (message, error=false) => { const el=document.getElementById('wcToast'); el.classList.toggle('text-bg-danger', error); el.classList.toggle('text-bg-success', !error); document.getElementById('wcToastText').textContent=message; bootstrap.Toast.getOrCreateInstance(el).show(); };
async function request(path, options={}) {
const response = await fetch(api + path, {credentials:'include', ...options});
if (!response.ok) { let detail='Handlingen mislykkedes'; try { detail=(await response.json()).detail || detail; } catch {} throw new Error(detail); }
return response.status === 204 ? null : response.json();
}
async function load() {
const list=document.getElementById('contentList'); list.innerHTML='<div class="wc-empty"><div class="spinner-border spinner-border-sm me-2"></div>Indlæser…</div>';
try { items=await request(`/${tab}`); render(); document.getElementById('connectionState').innerHTML='<i class="bi bi-check-circle-fill me-1"></i> Website-database forbundet'; }
catch(error) { list.innerHTML=`<div class="wc-empty text-danger"><i class="bi bi-exclamation-triangle fs-3 d-block mb-2"></i>${esc(error.message)}</div>`; document.getElementById('connectionState').innerHTML='<i class="bi bi-x-circle-fill me-1"></i> Ikke forbundet'; }
}
function render() {
const list=document.getElementById('contentList');
if (!items.length) { list.innerHTML='<div class="wc-empty">Der er endnu ikke oprettet indhold her.</div>'; return; }
list.innerHTML=items.map(item => tab==='customers' ? customerRow(item) : operationRow(item)).join('');
list.querySelectorAll('[data-edit]').forEach(button => button.onclick=()=>openEditor(Number(button.dataset.edit)));
list.querySelectorAll('[data-toggle]').forEach(button => button.onclick=()=>toggleVisibility(Number(button.dataset.toggle)));
list.querySelectorAll('[data-complete]').forEach(button => button.onclick=()=>complete(Number(button.dataset.complete)));
}
function customerRow(item) {
const active=!!item.is_active, preview=`${api}/customers/${item.id}/logo?v=${encodeURIComponent(item.updated_at || '')}`;
return `<article class="wc-row ${active?'':'wc-hidden'}"><div class="d-flex align-items-center gap-3"><img class="wc-logo" src="${preview}" onerror="this.onerror=null;this.src='${esc(item.logo_url || '')}'" alt=""><div><div class="fw-semibold">${esc(item.customer_name)}</div><div class="wc-meta">Placering ${item.sort_order} · ${active?'Synlig':'Skjult'}</div>${item.website_url?`<a class="small" href="${esc(item.website_url)}" target="_blank" rel="noopener">${esc(item.website_url)}</a>`:''}</div></div>${actions(item, active)}`;
}
function operationRow(item) {
const visibility=tab==='operations' ? !!item.is_active : !!item.is_public;
return `<article class="wc-row ${visibility?'':'wc-hidden'}"><div><div class="d-flex flex-wrap align-items-center gap-2 mb-1"><span class="wc-severity ${item.severity}">${esc(item.severity)}</span><strong>${esc(item.title)}</strong></div><div class="mb-2">${esc(item.message)}</div><div class="wc-meta">${formatDate(item.starts_at)} → ${formatDate(item.ends_at)} · ${visibility?(tab==='operations'?'Aktiv':'Offentlig'):'Skjult'}</div></div>${actions(item, visibility)}`;
}
function actions(item, visible) {
if (!canEdit) return '';
return `<div class="wc-actions d-flex gap-2"><button class="btn btn-sm btn-outline-secondary" data-edit="${item.id}"><i class="bi bi-pencil"></i><span class="visually-hidden">Redigér</span></button>${tab==='operations'&&item.is_active?`<button class="btn btn-sm btn-success" data-complete="${item.id}"><i class="bi bi-check2-circle me-1"></i>Afslut</button>`:''}<button class="btn btn-sm btn-outline-secondary" data-toggle="${item.id}">${visible?'Skjul':'Vis'}</button></div>`;
}
function openEditor(id=null) {
editing=id ? items.find(item=>item.id===id) : null;
document.getElementById('editorTitle').textContent=(editing?'Redigér ':'Opret ') + labels[tab][0].toLowerCase();
document.getElementById('editorFields').innerHTML=tab==='customers' ? customerFields(editing||{}) : operationFields(editing||{});
modal.show();
}
function customerFields(v) { return `<div class="row g-3"><div class="col-md-8"><label class="form-label">Kundenavn</label><input class="form-control" name="customer_name" maxlength="180" required value="${esc(v.customer_name)}"></div><div class="col-md-4"><label class="form-label">Rækkefølge</label><input class="form-control" type="number" min="0" name="sort_order" value="${v.sort_order ?? 100}"></div><div class="col-12"><label class="form-label">Website-link</label><input class="form-control" type="url" name="website_url" value="${esc(v.website_url)}" placeholder="https://…"></div><div class="col-12"><label class="form-label">Logo</label><input class="form-control" type="file" name="logo" accept="image/png,image/jpeg,image/webp,image/gif"><div class="form-text">PNG, JPEG, WebP eller GIF · maks. 5 MB. Tomt felt beholder eksisterende logo.</div><img id="logoPreview" class="wc-logo mt-2 d-none" alt="Logo-preview"></div><div class="col-12 form-check ms-2"><input class="form-check-input" type="checkbox" name="is_active" id="activeCheck" ${v.is_active===0?'':'checked'}><label class="form-check-label" for="activeCheck">Synlig på websitet</label></div></div>`; }
function operationFields(v) { const visibility=tab==='operations'?'is_active':'is_public'; return `<div class="row g-3"><div class="col-md-8"><label class="form-label">Titel</label><input class="form-control" name="title" maxlength="180" required value="${esc(v.title)}"></div><div class="col-md-4"><label class="form-label">Severity</label><select class="form-select" name="severity">${['ok','info','warning','critical'].map(x=>`<option ${v.severity===x?'selected':''}>${x}</option>`).join('')}</select></div><div class="col-12"><label class="form-label">Meddelelse</label><textarea class="form-control" name="message" rows="5" required>${esc(v.message)}</textarea></div><div class="col-md-6"><label class="form-label">Planlagt start</label><input class="form-control" type="datetime-local" name="starts_at" value="${localInput(v.starts_at)}"></div><div class="col-md-6"><label class="form-label">Planlagt slut</label><input class="form-control" type="datetime-local" name="ends_at" value="${localInput(v.ends_at)}"></div><div class="col-12 form-check ms-2"><input class="form-check-input" type="checkbox" name="${visibility}" id="visibleCheck" ${v[visibility]===0?'':'checked'}><label class="form-check-label" for="visibleCheck">${tab==='operations'?'Aktiv':'Offentlig'}</label></div></div>`; }
document.getElementById('editorFields').addEventListener('change', event => { if(event.target.name==='logo'&&event.target.files[0]) { const p=document.getElementById('logoPreview'); p.src=URL.createObjectURL(event.target.files[0]); p.classList.remove('d-none'); } });
document.getElementById('editorForm').onsubmit=async event => {
event.preventDefault(); const form=new FormData(event.target), path=`/${tab}${editing?'/'+editing.id:''}`;
const payload=tab==='customers' ? {customer_name:form.get('customer_name'), website_url:form.get('website_url')||null, sort_order:Number(form.get('sort_order')), is_active:form.has('is_active')} : {title:form.get('title'), severity:form.get('severity'), message:form.get('message'), starts_at:form.get('starts_at')||null, ends_at:form.get('ends_at')||null, [tab==='operations'?'is_active':'is_public']:form.has(tab==='operations'?'is_active':'is_public')};
try { const saved=await request(path,{method:editing?'PATCH':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}); const logo=form.get('logo'); if(tab==='customers'&&logo&&logo.size){const data=new FormData();data.append('logo',logo);await request(`/customers/${saved.id}/logo`,{method:'POST',body:data});} modal.hide(); toast('Indholdet er gemt'); await load(); } catch(error){toast(error.message,true);}
};
async function toggleVisibility(id) { const item=items.find(x=>x.id===id), field=tab==='incidents'?'is_public':'is_active'; try{await request(`/${tab}/${id}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({[field]:!item[field]})});await load();}catch(error){toast(error.message,true);} }
async function complete(id) { if(!confirm('Afslut hændelsen og flyt den til historikken?'))return; try{await request(`/operations/${id}/complete`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({is_public:true})});toast('Hændelsen er flyttet til historikken');await load();}catch(error){toast(error.message,true);} }
document.querySelectorAll('[data-tab]').forEach(button=>button.onclick=()=>{tab=button.dataset.tab;document.querySelectorAll('[data-tab]').forEach(x=>x.className='btn btn-outline-secondary');button.className='btn btn-primary';document.getElementById('panelTitle').textContent=labels[tab][0];document.getElementById('panelSubtitle').textContent=labels[tab][1];load();});
document.getElementById('createBtn')?.addEventListener('click',()=>openEditor()); load();
})();
</script>
{% endblock %}