Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
85 lines
4.4 KiB
HTML
85 lines
4.4 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Admin — brugere{% endblock %}
|
|
{% block body %}
|
|
<h1>Brugere</h1>
|
|
<p class="lead">Opret og administrér brugere. Adgang til services styres via <strong class="text-text">grupper</strong>.</p>
|
|
{% if saved %}<div class="flash">{{ saved }}</div>{% endif %}
|
|
|
|
<details style="margin:1rem 0">
|
|
<summary class="btn" style="display:inline-block">+ Opret ny bruger</summary>
|
|
<form method="post" action="/admin/users" class="stat" style="display:block;margin-top:.8rem">
|
|
<div style="display:grid;gap:.7rem;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr))">
|
|
<label>Brugernavn<input name="username" required autocomplete="off"></label>
|
|
<label>Navn<input name="name" autocomplete="off"></label>
|
|
<label>Email<input name="email" type="email" autocomplete="off"></label>
|
|
<label>Adgangskode<input name="password" type="text" autocomplete="new-password" placeholder="(valgfri)"></label>
|
|
</div>
|
|
<p class="muted" style="margin:.9rem 0 .4rem;font-size:.85rem">Adgang (grupper) — sæt flueben for de services brugeren skal kunne tilgå:</p>
|
|
<div style="display:grid;gap:.3rem .9rem;grid-template-columns:repeat(auto-fill,minmax(11rem,1fr))">
|
|
{% for grp in groups %}
|
|
<label style="display:flex;gap:.4rem;align-items:center;font-size:.9rem"><input type="checkbox" name="groups" value="{{ grp.pk }}" style="width:auto"> {{ grp.name }}</label>
|
|
{% endfor %}
|
|
</div>
|
|
<p style="margin-top:1rem"><button class="btn" type="submit">Opret bruger</button></p>
|
|
</form>
|
|
</details>
|
|
|
|
<div class="toolbar">
|
|
<div class="filter" style="flex:1;min-width:12rem"><span class="mag">⌕</span>
|
|
<input id="ufilter" type="search" placeholder="Søg brugere…" autocomplete="off"></div>
|
|
<div class="viewtoggle" id="typetoggle" role="group" aria-label="Brugertype">
|
|
<button type="button" data-type="person" class="on">Personer</button>
|
|
<button type="button" data-type="service">Service-konti</button>
|
|
<button type="button" data-type="all">Alle</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ucards" id="ucards" style="margin-top:1rem">
|
|
{% for u in users %}
|
|
{% set svc = u.type in ['service_account','internal_service_account'] %}
|
|
<a class="ucard nav{% if not u.active %} dim{% endif %}" href="/admin/users/{{ u.pk }}"
|
|
data-type="{{ 'service' if svc else 'person' }}"
|
|
data-search="{{ (u.username ~ ' ' ~ (u.name or '') ~ ' ' ~ (u.email or ''))|lower }}">
|
|
<span class="uini">{{ (u.username or '?')[0]|upper }}</span>
|
|
<span class="ubody">
|
|
<span class="uname">{{ u.name or u.username }}</span>
|
|
<span class="usub" title="{{ u.username }}{% if u.email %} · {{ u.email }}{% endif %}">{{ u.username }}{% if u.group_names %} · {{ u.group_names|length }} grupper{% endif %}{% if not u.active %} · inaktiv{% endif %}</span>
|
|
</span>
|
|
<span class="unav">→</span>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
<p id="unone" class="muted" style="display:none;margin-top:1rem">Ingen brugere matcher.</p>
|
|
|
|
<style>
|
|
.toolbar { display:flex; gap:1rem; align-items:center; flex-wrap:wrap; margin:1.4rem 0 .2rem; }
|
|
.viewtoggle { display:inline-flex; border:1px solid var(--border); border-radius:.7rem; overflow:hidden; }
|
|
.viewtoggle button { background:transparent; color:var(--muted); border:0; padding:.5rem .9rem; cursor:pointer; font:inherit; font-size:.85rem; }
|
|
.viewtoggle button.on { background:var(--surface2); color:var(--text); }
|
|
.ucard.nav { text-decoration:none; color:inherit; }
|
|
.ucard.nav .unav { flex:0 0 auto; color:var(--accent); opacity:.7; }
|
|
.ucard.dim { opacity:.55; }
|
|
</style>
|
|
<script>
|
|
(function(){
|
|
var inp=document.getElementById('ufilter'), tt=document.getElementById('typetoggle');
|
|
var cards=[].slice.call(document.querySelectorAll('#ucards .ucard'));
|
|
var none=document.getElementById('unone'), type='person';
|
|
function apply(){
|
|
var q=(inp.value||'').trim().toLowerCase(), shown=0;
|
|
cards.forEach(function(c){
|
|
var okType=(type==='all')||c.getAttribute('data-type')===type;
|
|
var okSearch=!q||c.getAttribute('data-search').indexOf(q)>-1;
|
|
var hit=okType&&okSearch; c.classList.toggle('hidden',!hit); if(hit) shown++;
|
|
});
|
|
none.style.display=shown?'none':'block';
|
|
}
|
|
inp.addEventListener('input',apply);
|
|
tt.addEventListener('click',function(e){var b=e.target.closest('button[data-type]'); if(!b)return;
|
|
type=b.getAttribute('data-type');
|
|
tt.querySelectorAll('button').forEach(function(x){x.classList.toggle('on',x===b);}); apply();});
|
|
apply();
|
|
})();
|
|
</script>
|
|
{% endblock %}
|