1daf8db470
Introduces a `resolve_return_path` function to determine the redirect target after a case action. This function prioritizes the `Referer` header for a more contextual redirect, falling back to the default case list page if the header is absent or invalid. Additionally, this commit refactors the UI elements for actions on the case detail and case list pages, improving organization and clarity.
174 lines
8.7 KiB
HTML
174 lines
8.7 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Doctate — Meine Fälle</title>
|
|
<style>
|
|
body { font-family: sans-serif; max-width: 900px; margin: 2em auto; padding: 0 1em; }
|
|
header { display: flex; justify-content: space-between; align-items: center; }
|
|
header form { margin: 0; }
|
|
section { margin: 1.5em 0; }
|
|
section h2 { font-size: 1.1em; color: #555; border-bottom: 1px solid #ddd; padding-bottom: 0.2em; }
|
|
.empty { color: #888; font-style: italic; }
|
|
|
|
.case-list { list-style: none; padding: 0; margin: 0; }
|
|
.case-row { display: grid; grid-template-columns: auto 1fr auto; gap: 0.6em 1em; align-items: center; padding: 0.7em 1em; border: 1px solid #ccc; border-radius: 4px; margin: 0.5em 0; }
|
|
.case-row.done { border-left: 4px solid #7ed321; }
|
|
.case-row.open { border-left: 4px solid #4a90e2; }
|
|
.case-row .check { align-self: start; padding-top: 0.25em; }
|
|
.case-row .body { min-width: 0; }
|
|
.case-row .body a { color: inherit; text-decoration: none; }
|
|
.case-row .body a:hover .line1 { text-decoration: underline; }
|
|
.case-row .body h3 { margin: 0 0 0.2em 0; font-size: 1em; font-weight: 600; }
|
|
.case-row .body small { color: #666; font-family: monospace; font-size: 0.85em; }
|
|
.case-row .oneliner { color: #333; margin: 0.2em 0 0; }
|
|
.case-row .line1 { font-size: 1em; margin: 0; }
|
|
.case-row .line1 .time { font-family: monospace; font-weight: bold; color: #444; }
|
|
.case-row .line2 { margin: 0.2em 0 0; color: #555; font-size: 0.95em; display: flex; align-items: center; gap: 0.5em; }
|
|
.case-row .uuid { color: #999; font-family: monospace; font-size: 0.7em; margin-top: 0.2em; }
|
|
.status-badge { display: inline-block; padding: 0.05em 0.5em; border-radius: 999px; font-size: 0.75em; font-weight: bold; color: white; }
|
|
.status-badge.open { background: #e24a4a; }
|
|
.status-badge.done { background: #7ed321; }
|
|
.case-row .actions { display: flex; gap: 0.4em; align-items: center; }
|
|
.case-row .actions button { font-size: 0.85em; padding: 0.35em 0.8em; }
|
|
.case-row .actions .delete-btn { padding: 0.3em; background: transparent; border: none; color: #888; cursor: pointer; display: inline-flex; align-items: center; line-height: 0; border-radius: 4px; transition: color 0.15s, background 0.15s; }
|
|
.case-row .actions .delete-btn:hover { color: #e24a4a; background: #fff0f0; }
|
|
|
|
.label { display: inline-block; margin-left: 0.6em; padding: 0.05em 0.5em; border-radius: 999px; font-size: 0.75em; font-weight: bold; vertical-align: middle; }
|
|
.label.analyzing { background: #eee; color: #555; }
|
|
.label.done-doc { background: #7ed321; color: white; }
|
|
|
|
.bulk-bar { margin: 1em 0; padding: 0.8em 1em; background: #f6f6f6; border-radius: 4px; display: flex; gap: 0.6em; flex-wrap: wrap; align-items: center; }
|
|
.bulk-bar button { font-size: 0.9em; padding: 0.5em 1em; }
|
|
.bulk-bar .undo { margin-left: auto; }
|
|
.bulk-bar .undo button { background: #fff3cd; border: 1px solid #d39e00; color: #5a4500; }
|
|
|
|
.header-right { display: flex; align-items: center; gap: 0.8em; }
|
|
.admin-toggle { font-size: 0.85em; color: #666; display: inline-flex; align-items: center; gap: 0.3em; cursor: pointer; user-select: none; }
|
|
html.admin-view-off .admin-only { display: none !important; }
|
|
</style>
|
|
<script>
|
|
try {
|
|
if (localStorage.getItem('admin-view') === 'off') {
|
|
document.documentElement.classList.add('admin-view-off');
|
|
}
|
|
} catch (_) {}
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>{{ slug }} — Meine Fälle</h1>
|
|
<div class="header-right">
|
|
{% if is_admin %}<label class="admin-toggle"><input type="checkbox" id="admin-view-toggle"> Admin view</label>{% endif %}
|
|
<form method="post" action="/web/logout"><button type="submit">Logout</button></form>
|
|
</div>
|
|
</header>
|
|
|
|
{% if undo_count > 0 %}
|
|
<form class="bulk-bar" method="post" action="/web/cases/undo-delete" style="background:#fff8e1;">
|
|
<span><strong>{{ undo_count }}</strong> zuletzt gelöschte(r) Fall(/Fälle).</span>
|
|
<button type="submit">Löschung rückgängig</button>
|
|
</form>
|
|
{% endif %}
|
|
|
|
{% if total == 0 %}
|
|
<section><p class="empty">Keine Fälle.</p></section>
|
|
{% else %}
|
|
<form method="post" action="/web/cases/bulk">
|
|
{% for g in groups %}
|
|
<section>
|
|
<h2 class="date-group" data-iso="{{ g.label }}">{{ g.label }} ({{ g.cases.len() }})</h2>
|
|
<ul class="case-list">
|
|
{% for case in g.cases %}
|
|
<li class="case-row {% if case.has_document %}done{% else %}open{% endif %}">
|
|
<div class="check"><input type="checkbox" name="case_id" value="{{ case.case_id }}"></div>
|
|
<div class="body">
|
|
<a href="/web/cases/{{ case.case_id }}">
|
|
<div class="line1"><span class="time"><time datetime="{{ case.recorded_at_iso }}">{{ case.time_hms_utc }}</time></span>{% match case.oneliner %}{% when Some with (t) %} — {{ t }}{% when None %} — <span class="empty">kein Oneliner</span>{% endmatch %}</div>
|
|
<div class="line2">{{ case.recordings_count }} Aufnahmen{% if case.has_document %} — <span class="status-badge done">ausgewertet</span>{% else %} — <span class="status-badge open">offen</span>{% endif %}{% if case.analyzing %} <span class="label analyzing">wird analysiert</span>{% endif %}</div>
|
|
{% if is_admin %}<div class="uuid admin-only">{{ case.case_id }}</div>{% endif %}
|
|
</a>
|
|
</div>
|
|
<div class="actions">
|
|
{% if is_admin %}<button type="submit" formaction="/web/cases/{{ case.case_id }}/analyze" class="admin-only"{% if !case.can_analyze %} disabled{% endif %}>{% if case.has_document %}Neu analysieren{% else %}Analysieren{% endif %}</button>
|
|
<button type="submit" formaction="/web/cases/{{ case.case_id }}/reset" class="admin-only">Reset</button>{% endif %}
|
|
<button type="submit" formaction="/web/cases/{{ case.case_id }}/delete" class="delete-btn" title="Fall entfernen" aria-label="Fall entfernen"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6"></path><path d="M14 11v6"></path><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"></path></svg></button>
|
|
</div>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</section>
|
|
{% endfor %}
|
|
<div class="bulk-bar">
|
|
<span>Auswahl:</span>
|
|
<button type="button" onclick="const b=document.querySelectorAll('input[name=case_id]');const all=Array.from(b).every(c=>c.checked);b.forEach(c=>c.checked=!all);this.textContent=all?'Alles auswählen':'Keine auswählen'">Alles auswählen</button>
|
|
{% if is_admin %}<button type="submit" name="action" value="analyze" class="admin-only">Analysieren</button>
|
|
<button type="submit" name="action" value="reset" class="admin-only">Reset</button>{% endif %}
|
|
<button type="submit" name="action" value="delete">Entfernen</button>
|
|
</div>
|
|
</form>
|
|
{% endif %}
|
|
<script>
|
|
// Render all UTC timestamps in the browser's local timezone, and relabel
|
|
// date-group headings to Heute/Gestern based on the browser's current
|
|
// UTC date. Server-side renders only UTC so timezone logic belongs here.
|
|
(function () {
|
|
document.querySelectorAll('time[datetime]').forEach(function (el) {
|
|
var d = new Date(el.dateTime);
|
|
if (!isNaN(d.getTime())) {
|
|
el.textContent = d.toLocaleTimeString(undefined, {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: false,
|
|
});
|
|
}
|
|
});
|
|
|
|
var todayISO = new Date().toISOString().slice(0, 10);
|
|
var yesterday = new Date();
|
|
yesterday.setUTCDate(yesterday.getUTCDate() - 1);
|
|
var yesterdayISO = yesterday.toISOString().slice(0, 10);
|
|
|
|
document.querySelectorAll('h2.date-group').forEach(function (h) {
|
|
var iso = h.dataset.iso;
|
|
if (!iso) return;
|
|
var suffix = h.textContent.replace(iso, '').trim();
|
|
var label;
|
|
if (iso === todayISO) label = 'Heute';
|
|
else if (iso === yesterdayISO) label = 'Gestern';
|
|
else label = iso;
|
|
h.textContent = label + ' ' + suffix;
|
|
});
|
|
})();
|
|
|
|
(() => {
|
|
const cb = document.getElementById('admin-view-toggle');
|
|
if (!cb) return;
|
|
cb.checked = !document.documentElement.classList.contains('admin-view-off');
|
|
cb.addEventListener('change', () => {
|
|
const on = cb.checked;
|
|
try { localStorage.setItem('admin-view', on ? 'on' : 'off'); } catch (_) {}
|
|
document.documentElement.classList.toggle('admin-view-off', !on);
|
|
});
|
|
})();
|
|
|
|
// Live-update bus: any case event for this user triggers a debounced
|
|
// page reload. Debounce collapses event bursts (e.g. bulk-delete) into
|
|
// one reload. EventSource auto-reconnects on network drops.
|
|
(() => {
|
|
let timer = null;
|
|
const scheduleReload = () => {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => location.reload(), 300);
|
|
};
|
|
const es = new EventSource('/web/events');
|
|
// Release the socket-pool slot on navigation. Without this, Chrome
|
|
// keeps the SSE connection "draining" after unload; six rapid nav
|
|
// cycles exhaust the 6-per-origin pool and stall further requests.
|
|
window.addEventListener('pagehide', () => es.close());
|
|
es.addEventListener('case', () => scheduleReload());
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|