admin: kort-visning af brugere m. type-filter (skjul service-konti) + kort-baseret gruppe-medlemskab
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,50 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Admin — {{ g.name }}{% endblock %}
|
||||
|
||||
{% macro ucard(u) %}
|
||||
<label class="ucard" data-search="{{ (u.username ~ ' ' ~ (u.name or '') ~ ' ' ~ (u.email or ''))|lower }}">
|
||||
<input type="checkbox" name="members" value="{{ u.pk }}" {% if u.pk|string in g.users %}checked{% endif %}>
|
||||
<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.email %} · {{ u.email }}{% endif %}</span>
|
||||
</span>
|
||||
<span class="uchk">✓</span>
|
||||
</label>
|
||||
{% endmacro %}
|
||||
|
||||
{% block body %}
|
||||
<p><a href="/admin/groups" class="muted">← Grupper</a></p>
|
||||
<h1>{{ g.name }}</h1>
|
||||
<p class="lead">Sæt flueben ved de brugere der skal være medlem af gruppen.</p>
|
||||
<p class="lead">Klik på brugerne der skal være medlem. Markerede er medlemmer.</p>
|
||||
|
||||
{% set people = users|rejectattr('type','in',['service_account','internal_service_account'])|list %}
|
||||
{% set svcs = users|selectattr('type','in',['service_account','internal_service_account'])|list %}
|
||||
|
||||
<form method="post" action="/admin/groups/{{ g.pk }}">
|
||||
<div style="display:grid;gap:.3rem .9rem;grid-template-columns:repeat(auto-fill,minmax(13rem,1fr));margin-top:1rem">
|
||||
{% for u in users %}
|
||||
<label style="display:flex;gap:.4rem;align-items:center;font-size:.9rem"><input type="checkbox" name="members" value="{{ u.pk }}" {% if u.pk|string in g.users %}checked{% endif %} style="width:auto"> {{ u.username }}{% if u.name %} <span class="muted">({{ u.name }})</span>{% endif %}</label>
|
||||
{% endfor %}
|
||||
<div class="filter" style="max-width:22rem">
|
||||
<span class="mag">⌕</span>
|
||||
<input id="ufilter" type="search" placeholder="Søg brugere…" autocomplete="off" aria-label="Søg brugere">
|
||||
</div>
|
||||
<p style="margin-top:1.2rem"><button class="btn" type="submit">Gem medlemmer</button></p>
|
||||
<div class="ucards" id="ucards">
|
||||
{% for u in people %}{{ ucard(u) }}{% endfor %}
|
||||
</div>
|
||||
{% if svcs %}
|
||||
<details style="margin-top:1.2rem">
|
||||
<summary class="muted" style="cursor:pointer">Service-konti ({{ svcs|length }})</summary>
|
||||
<div class="ucards">{% for u in svcs %}{{ ucard(u) }}{% endfor %}</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
<p style="margin-top:1.3rem"><button class="btn" type="submit">Gem medlemmer</button></p>
|
||||
</form>
|
||||
<script>
|
||||
(function(){
|
||||
var inp=document.getElementById('ufilter'); if(!inp) return;
|
||||
var cards=[].slice.call(document.querySelectorAll('#ucards .ucard'));
|
||||
inp.addEventListener('input',function(){
|
||||
var q=inp.value.trim().toLowerCase();
|
||||
cards.forEach(function(c){ c.classList.toggle('hidden', !!q && c.getAttribute('data-search').indexOf(q)<0); });
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
+74
-30
@@ -2,39 +2,83 @@
|
||||
{% 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> (en app er bundet til en gruppe).</p>
|
||||
<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 %}
|
||||
|
||||
<h2>Opret ny bruger</h2>
|
||||
<form method="post" action="/admin/users" class="stat" style="display:block">
|
||||
<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 g in groups %}
|
||||
<label style="display:flex;gap:.4rem;align-items:center;font-size:.9rem"><input type="checkbox" name="groups" value="{{ g.pk }}" style="width:auto"> {{ g.name }}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p style="margin-top:1rem"><button class="btn" type="submit">Opret bruger</button></p>
|
||||
</form>
|
||||
<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>
|
||||
|
||||
<h2>Alle brugere ({{ users|length }})</h2>
|
||||
<table>
|
||||
<thead><tr><th>Bruger</th><th>Email</th><th>Grupper</th><th>Status</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<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 %}
|
||||
<tr>
|
||||
<td><strong>{{ u.username }}</strong>{% if u.name %} <span class="muted">· {{ u.name }}</span>{% endif %}</td>
|
||||
<td class="muted">{{ u.email or '—' }}</td>
|
||||
<td class="muted" style="font-size:.85rem">{{ u.group_names|join(', ') if u.group_names else '—' }}</td>
|
||||
<td>{% if u.active %}<span style="color:var(--accent)">aktiv</span>{% else %}<span class="muted">inaktiv</span>{% endif %}</td>
|
||||
<td><a href="/admin/users/{{ u.pk }}">Rediger →</a></td>
|
||||
</tr>
|
||||
{% 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 %}
|
||||
</tbody>
|
||||
</table>
|
||||
</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 %}
|
||||
|
||||
@@ -113,6 +113,23 @@
|
||||
code { background:var(--surface2); padding:.12em .42em; border-radius:.35rem; font-size:.85em; }
|
||||
.stat { background:color-mix(in srgb, var(--surface) 70%, transparent); border:1px solid var(--border); border-radius:1rem; padding:1.3rem; }
|
||||
.stat .n { font-size:2.1rem; font-weight:800; letter-spacing:-.02em; }
|
||||
/* bruger-kort (medlemskab/tildeling) — fixed bredde + afkortning, intet overlap */
|
||||
.ucards { display:grid; gap:.55rem; grid-template-columns:repeat(auto-fill,minmax(15rem,1fr)); margin-top:.6rem; }
|
||||
.ucard { position:relative; display:flex; align-items:center; gap:.6rem; padding:.55rem .7rem; border:1px solid var(--border);
|
||||
border-radius:.8rem; background:color-mix(in srgb,var(--surface) 70%,transparent); cursor:pointer; min-width:0;
|
||||
transition:border-color .15s, background .15s; }
|
||||
.ucard:hover { border-color:var(--accent); }
|
||||
.ucard input { position:absolute; opacity:0; pointer-events:none; }
|
||||
.ucard:has(input:checked) { border-color:var(--accent); background:color-mix(in srgb,var(--accent) 12%,transparent); }
|
||||
.ucard .uini { flex:0 0 auto; width:1.9rem; height:1.9rem; border-radius:.5rem; display:grid; place-items:center;
|
||||
font-weight:700; font-size:.85rem; background:color-mix(in srgb,var(--accent) 16%,transparent); color:var(--accent); }
|
||||
.ucard .ubody { min-width:0; flex:1; }
|
||||
.ucard .uname { font-weight:600; font-size:.9rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.ucard .usub { color:var(--muted); font-size:.78rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.ucard .uchk { flex:0 0 auto; width:1.25rem; height:1.25rem; border:1.5px solid var(--border); border-radius:.35rem;
|
||||
display:grid; place-items:center; color:transparent; font-size:.8rem; font-weight:700; }
|
||||
.ucard:has(input:checked) .uchk { background:var(--accent); border-color:var(--accent); color:#06101f; }
|
||||
.ucard.hidden { display:none; }
|
||||
/* footer */
|
||||
.foot { width:100%; max-width:960px; margin:0 auto; padding:1.6rem 1.25rem 2.5rem; color:var(--muted); font-size:.85rem; border-top:1px solid var(--border); }
|
||||
.foot a { color:var(--accent); text-decoration:none; }
|
||||
|
||||
Reference in New Issue
Block a user