Karten-Akkordeon fuer rtable: nur eine Karte offen, Rest gestapelt
Auf Tablet/Handy sollen die Tabellen-zu-Karten-Ansichten weniger Scrollen erfordern: jetzt ist immer nur eine Karte aufgeklappt, alle anderen sind auf eine kompakte Kopfzeile reduziert und stapeln sich eng darunter. - Neue rtInit()-Helper-Funktion: erste Zeile startet offen, Klick auf eine Zeile oeffnet sie und schliesst alle anderen (Klicks auf verschachtelte Buttons/Links/Inputs/.tl loesen die Akkordeon-Logik nicht aus, damit Aktionen weiter unabhaengig funktionieren) - Erste data-label-Zelle pro Tabelle als .rt-head markiert (mit Chevron-Icon), bleibt immer sichtbar; restliche data-label-Zellen nur bei .rt-open sichtbar. Zellen ohne data-label (Aktions-Buttons) bleiben immer erreichbar, auch bei zugeklappter Karte. - rtInit() nach jedem rtable-Render aufgerufen: pgDash, renderPT (inkl. Filter-Refresh), pgAufgaben, renderAdminDash, loadPat - Bugfix unterwegs: Chevron-Icon leckte in die normale Desktop/Tablet- Tabelle (display:none als Basis ergaenzt, nur im 700px-Breakpoint sichtbar) - vor dem Fix bei 820px Tablet-Breite als stoerendes Icon unter jedem Namen sichtbar Verifiziert per Playwright auf 390px (Handy) und 820px (Tablet): Standardzustand (erste Karte offen), Wechsel beim Klick auf andere Karte, saubere Tabelle ohne Akkordeon-Artefakte oberhalb 700px.
This commit is contained in:
1 parent
9ebb8f008a
commit
8dd141ea35
2 files changed
+42
-7
No files matched your search
@@ -69,6 +69,7 @@ The frontend was originally desktop-only (zero `@media` queries, fixed 228px sid
|
||||
- **Glass design system**: `:root` defines a dedicated token set — `--glass`/`--glass-strong` (translucent white fills), `--glass-border`, `--glass-blur`, `--glass-shadow`/`--glass-shadow-lg`, plus four `--bento-*` gradient tokens (mint/lavender/peach/rose) for tile accents. `body` has a fixed multi-blob `radial-gradient` background (`background-attachment:fixed`) that the translucent, `backdrop-filter:blur(...)` cards/sidebar/topbar/modal/login-box float over. Regular form inputs stay solid (`var(--surface)`, opaque white) — only container-level chrome (`.card`, `.modal`, `.sb`, `.topbar`, `.lo-box`) is glassy, intentionally, for text legibility. Always pair `backdrop-filter` with `-webkit-backdrop-filter` (Safari).
|
||||
- **Dashboard is a real Bento grid**, not just a relabeled stat row: `.bento` is a 4-column CSS grid with **explicit** `grid-column`/`grid-row` line placement per tile class (`.bento-stat`, `.bento-hero`, `.bento-aufg`, `.bento-quick`) — deliberately not relying on grid auto-placement, which does not reliably fill remaining same-row cells the way you'd expect when items of different spans are mixed. If you add/resize tiles, keep using explicit line numbers (or verify auto-placement with the debug technique below) rather than assuming `span N` will pack tiles the way you expect.
|
||||
- **Responsive tables → cards**: any live-UI (non-PDF-export) table that needs to stay usable at phone width gets `class="tw rtable"` on its wrapper and `data-label="…"` on every `<td>`. Below 700px, `.rtable` CSS turns each `<tr>` into its own glass card (label stacked above value per cell, via `td[data-label]::before{content:attr(data-label)}`) instead of squeezing table columns. Applied to: Dashboard's recent-patients table, `renderPT` (Patientenliste), `pgAufgaben`, `renderAdminDash`'s Konten table, and the Patientenakte Anträge table. Follow this pattern for any new data table — don't ship a raw `<table>` without it.
|
||||
- **Accordion on the card view**: within `.rtable`, exactly one row is expanded at a time — the rest collapse to a single header line (`td.rt-head`, marked explicitly per table since it's usually the first `data-label` cell) and stack tightly, so scanning many rows needs far less scrolling. `rtInit()` (shared helper, defined near `escH`) wires this up: on load the first row gets `.rt-open`; clicking any row's header toggles it open and closes the others (clicks on nested `button,a,select,input,textarea,.tl` are ignored so action buttons/links inside a row still work independently of the accordion). **Every function that renders an `.rtable` must call `rtInit()` after the DOM is in place** (see `pgDash`, `renderPT`, `pgAufgaben`, `renderAdminDash`, `loadPat`) — a new table using this pattern that forgets the call will render but never expand/collapse. Cells without `data-label` (action-button columns) are never hidden by the accordion, so those buttons stay reachable even on a collapsed row. The `.rt-chev` chevron icon is `display:none` by default and only shown inside the `max-width:700px` breakpoint — without that base rule it leaks into the plain desktop/tablet `<table>` as a stray icon.
|
||||
- **Mobile/tablet nav**: below 900px the `.sb` sidebar becomes a fixed off-canvas panel (`transform:translateX(-100%)`, slides in via `.sb.open`), toggled by a hamburger button (`#sb-toggle`, only visible in that breakpoint) and a `#sb-backdrop` overlay that closes it on tap. `toggleSidebar()`/`closeSidebar()` manage this; every nav-item click auto-closes it. Form grids (`.fg.c2/.c3`), stat grids (`.g4`), and the bento grid collapse to fewer columns at 900px/700px/560px. Modals go full-screen below 560px. Form inputs are forced to `font-size:16px` on phones to stop iOS Safari's auto-zoom-on-focus.
|
||||
- **Verification gotcha #1 — internal scroll**: `.content` scrolls internally (`overflow-y:auto` inside a `height:100vh` shell), so Playwright's `page.screenshot({fullPage:true})` does **not** capture content below the fold — only the viewport. Scroll `.content` itself (`document.querySelector('.content').scrollTop = ...`) before screenshotting to see the rest.
|
||||
- **Verification gotcha #2 — stale template cache**: `start.py` runs with `debug=False`, so Flask's `TEMPLATES_AUTO_RELOAD` is off and Jinja caches the compiled `index.html` **in memory for the life of the process**, from the first request after startup. Editing the template while a `python start.py` process is still running from before your edit is invisible to the browser no matter how many times you reload — you'll see stale HTML/JS and it's easy to misdiagnose as a CSS/JS bug. **Always restart the server after editing `templates/index.html`** before taking verification screenshots or you'll debug a ghost.
|
||||
|
||||
@@ -154,15 +154,26 @@ body{
|
||||
.b-out{background:transparent;border:1px solid var(--border2);color:var(--muted)}
|
||||
.tw{overflow-x:auto}
|
||||
/* ── Responsive Tabellen: werden unter 700px zu Karten (rtable) ── */
|
||||
.rt-chev{display:none}
|
||||
@media (max-width:700px){
|
||||
.rtable table,.rtable thead,.rtable tbody,.rtable tr,.rtable td{display:block;width:100%}
|
||||
.rtable thead{display:none}
|
||||
.rtable tr{background:var(--glass);-webkit-backdrop-filter:blur(var(--glass-blur));backdrop-filter:blur(var(--glass-blur));border:1px solid var(--glass-border);border-radius:var(--r);margin-bottom:10px;padding:4px 12px;box-shadow:var(--glass-shadow)}
|
||||
.rtable tr{background:var(--glass);-webkit-backdrop-filter:blur(var(--glass-blur));backdrop-filter:blur(var(--glass-blur));border:1px solid var(--glass-border);border-radius:var(--r);margin-bottom:6px;padding:10px 12px;box-shadow:var(--glass-shadow);cursor:pointer}
|
||||
.rtable tr:last-child{margin-bottom:0}
|
||||
.rtable td{border:none;border-bottom:1px solid var(--border);padding:8px 0}
|
||||
.rtable td:last-child{border-bottom:none}
|
||||
.rtable td:empty{display:none}
|
||||
.rtable td[data-label]::before{content:attr(data-label);display:block;font-size:10px;font-weight:600;color:var(--hint);text-transform:uppercase;letter-spacing:.04em;margin-bottom:3px}
|
||||
/* Akkordeon: nur die offene Karte zeigt alle Felder, der Rest ist auf
|
||||
die Kopfzeile zusammengeklappt und stapelt sich eng - weniger Scrollen. */
|
||||
.rtable tr td[data-label]{display:none}
|
||||
.rtable tr td.rt-head{display:flex!important;align-items:center;justify-content:space-between;gap:8px;border-bottom:none;padding:0}
|
||||
.rtable tr td.rt-head::before{display:none}
|
||||
.rtable tr .rt-chev{display:inline-block;font-size:16px;color:var(--hint);transition:transform .18s;flex-shrink:0}
|
||||
.rtable tr.rt-open{padding-bottom:2px;box-shadow:var(--glass-shadow-lg);border-color:var(--green)}
|
||||
.rtable tr.rt-open td[data-label]{display:block}
|
||||
.rtable tr.rt-open td.rt-head{border-bottom:1px solid var(--border);padding:0 0 8px;margin-bottom:6px}
|
||||
.rtable tr.rt-open .rt-chev{transform:rotate(180deg);color:var(--green)}
|
||||
}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
th{text-align:left;padding:8px 11px;font-size:11.5px;font-weight:500;color:var(--muted);border-bottom:1px solid var(--border)}
|
||||
@@ -452,6 +463,24 @@ function toFilename(s){return(s||'').replace(/ä/g,'ae').replace(/ö/g,'oe').rep
|
||||
function isoDate(){return new Date().toISOString().slice(0,10);}
|
||||
function toast(m,ok=true){const t=document.createElement('div');t.className='toast';t.innerHTML='<i class="ti ti-'+(ok?'check':'alert-triangle')+'"></i>'+m;document.body.appendChild(t);setTimeout(()=>t.remove(),2800);}
|
||||
function escH(s){return(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||||
// Responsive Tabellen-Akkordeon (Mobile/Tablet): erste Karte offen, Rest
|
||||
// zugeklappt/gestapelt; Klick auf eine Karte öffnet sie und schliesst die
|
||||
// anderen. Nach jedem rtable-Render aufrufen (Filter/Refresh bauen neue
|
||||
// <tr>-Knoten, daher jedes Mal neu initialisieren statt einmalig).
|
||||
function rtInit(){
|
||||
document.querySelectorAll('.rtable tbody').forEach(tbody=>{
|
||||
const rows=tbody.querySelectorAll('tr');
|
||||
rows.forEach((tr,i)=>{
|
||||
if(i===0)tr.classList.add('rt-open');
|
||||
tr.onclick=e=>{
|
||||
if(e.target.closest('button,a,select,input,textarea,.tl'))return;
|
||||
const wasOpen=tr.classList.contains('rt-open');
|
||||
rows.forEach(r=>r.classList.remove('rt-open'));
|
||||
if(!wasOpen)tr.classList.add('rt-open');
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
function editNotizen(pid){
|
||||
const el=document.getElementById('notiz-display-'+pid);
|
||||
const current=el?el.dataset.notiz:'';
|
||||
@@ -1255,7 +1284,7 @@ function renderAdminDash(nutzer,log,patienten){
|
||||
:'<span class="badge b-gr">kein Zugang</span>');
|
||||
return '<tr>'+
|
||||
'<td>'+av(n.name||n.benutzername,n.id)+'</td>'+
|
||||
'<td data-label="Name"><div class="w5">'+escH(n.name||n.benutzername)+'</div><div class="sm muted">@'+escH(n.benutzername)+'</div></td>'+
|
||||
'<td data-label="Name" class="rt-head"><div><div class="w5">'+escH(n.name||n.benutzername)+'</div><div class="sm muted">@'+escH(n.benutzername)+'</div></div><i class="ti ti-chevron-down rt-chev"></i></td>'+
|
||||
'<td data-label="Rolle">'+(n.rolle==='admin'?'<span class="badge b-b">Admin</span>':'<span class="badge b-gr">Nutzer</span>')+'</td>'+
|
||||
'<td data-label="Status">'+(gesperrt?'<span class="badge b-r">Gesperrt</span>':'<span class="badge b-g">Aktiv</span>')+'</td>'+
|
||||
'<td data-label="Patienten">'+n.patienten_anzahl+'</td>'+
|
||||
@@ -1300,6 +1329,7 @@ function renderAdminDash(nutzer,log,patienten){
|
||||
logRows+
|
||||
'</div>',
|
||||
'Admin-Dashboard');
|
||||
rtInit();
|
||||
}
|
||||
function adminNutzerModal(uid,name,aktion){
|
||||
const titel={sperren:'Konto sperren',entsperren:'Konto entsperren',loeschen:'Konto löschen'}[aktion];
|
||||
@@ -1382,7 +1412,7 @@ async function pgDash(){
|
||||
|
||||
'<div class="card bento-hero"><div class="ch"><span class="ct">Aktuelle Patienten</span><button class="btn btn-sm" onclick="go(\'patienten\')">Alle</button></div>'+
|
||||
'<div class="tw rtable"><table><thead><tr><th>Name</th><th>Grad</th><th>Zielgruppe</th><th>Status</th></tr></thead><tbody>'+
|
||||
d.recent.map((p,i)=>'<tr><td data-label="Name"><div class="fl ic g8">'+av(p.vorname+' '+p.nachname,i)+'<span class="tl" onclick="loadPat('+p.id+')">'+p.vorname+' '+p.nachname+'</span></div></td><td data-label="Grad">'+
|
||||
d.recent.map((p,i)=>'<tr><td data-label="Name" class="rt-head"><div class="fl ic g8">'+av(p.vorname+' '+p.nachname,i)+'<span class="tl" onclick="loadPat('+p.id+')">'+p.vorname+' '+p.nachname+'</span></div><i class="ti ti-chevron-down rt-chev"></i></td><td data-label="Grad">'+
|
||||
(p.pflegegrad?'<span class="badge b-out">Grad '+p.pflegegrad+'</span>':'<span class="muted sm">–</span>')+'</td><td data-label="Zielgruppe">'+zg_badge(p.zielgruppe||'alterspflege')+'</td><td data-label="Status">'+bdg(p.status||'offen')+'</td></tr>').join('')+
|
||||
'</tbody></table></div></div>'+
|
||||
|
||||
@@ -1403,6 +1433,7 @@ async function pgDash(){
|
||||
'Dashboard',
|
||||
'<button class="btn btn-p" onclick="go(\'neu\')"><i class="ti ti-user-plus"></i>Neuer Patient</button>');
|
||||
checkTagebuchReminder();
|
||||
rtInit();
|
||||
}
|
||||
|
||||
// ── Patienten ─────────────────────────────────────────────
|
||||
@@ -1425,10 +1456,10 @@ function renderPT(data){
|
||||
const archiv=p.patient_status&&p.patient_status!=='aktiv';
|
||||
const stLabel={verstorben:'Verstorben',verzogen:'Verzogen',archiviert:'Archiviert'};
|
||||
return '<tr'+(archiv?' style="opacity:.72"':'')+'>'+
|
||||
'<td data-label="Name"><div class="fl ic g8">'+av(p.vorname+' '+p.nachname,i)+
|
||||
'<td data-label="Name" class="rt-head"><div class="fl ic g8">'+av(p.vorname+' '+p.nachname,i)+
|
||||
'<div><span class="tl" onclick="loadPat('+p.id+')">'+p.vorname+' '+p.nachname+'</span>'+
|
||||
(archiv?'<span class="badge b-gr" style="margin-left:6px;font-size:10px">'+(stLabel[p.patient_status]||p.patient_status)+'</span>':'')+
|
||||
'</div></div></td>'+
|
||||
'</div></div><i class="ti ti-chevron-down rt-chev"></i></td>'+
|
||||
'<td data-label="Geburtsdatum">'+fdate(p.geburtsdatum)+'</td>'+
|
||||
'<td data-label="Krankenkasse">'+(p.krankenkasse||'–')+'</td>'+
|
||||
'<td data-label="Grad">'+(p.pflegegrad?'<span class="badge b-out">Grad '+p.pflegegrad+'</span>':'<span class="muted">–</span>')+'</td>'+
|
||||
@@ -1439,6 +1470,7 @@ function renderPT(data){
|
||||
'</div></td></tr>';
|
||||
}).join('')+
|
||||
'</tbody></table></div>');
|
||||
rtInit();
|
||||
}
|
||||
async function filterPat(){
|
||||
let url='/api/patienten?';
|
||||
@@ -1502,7 +1534,7 @@ async function loadPat(id){
|
||||
'<div class="card"><div class="ch"><span class="ct">Anträge</span><button class="btn btn-sm" onclick="go(\'antrag\','+id+')"><i class="ti ti-plus"></i>Neu</button></div>'+
|
||||
(d.antraege.length===0?'<p class="muted sm">Noch kein Antrag.</p>':
|
||||
'<div class="tw rtable"><table><thead><tr><th>Typ</th><th>Datum</th><th>Grad</th><th>Status</th><th>Dokument</th><th></th></tr></thead><tbody>'+
|
||||
d.antraege.map(a=>'<tr><td data-label="Typ"><span class="tag">'+a.typ+'</span></td><td data-label="Datum">'+fdate(a.antragsdatum)+'</td><td data-label="Grad">'+(a.beantragter_pflegegrad?'Grad '+a.beantragter_pflegegrad:'–')+'</td><td data-label="Status">'+bdg(a.status)+'</td><td data-label="Dokument">'+(a.pdf_pfad?'<a href="/static/'+a.pdf_pfad+'" target="_blank" class="tl" style="white-space:nowrap"><i class="ti ti-file-type-pdf"></i> PDF öffnen</a>':'–')+'</td>'+
|
||||
d.antraege.map(a=>'<tr><td data-label="Typ" class="rt-head"><span class="tag">'+a.typ+'</span><i class="ti ti-chevron-down rt-chev"></i></td><td data-label="Datum">'+fdate(a.antragsdatum)+'</td><td data-label="Grad">'+(a.beantragter_pflegegrad?'Grad '+a.beantragter_pflegegrad:'–')+'</td><td data-label="Status">'+bdg(a.status)+'</td><td data-label="Dokument">'+(a.pdf_pfad?'<a href="/static/'+a.pdf_pfad+'" target="_blank" class="tl" style="white-space:nowrap"><i class="ti ti-file-type-pdf"></i> PDF öffnen</a>':'–')+'</td>'+
|
||||
'<td><button class="btn btn-sm" onclick="bescheidModal('+a.id+','+id+','+(a.beantragter_pflegegrad||1)+',\''+a.typ+'\')"><i class="ti ti-edit"></i>Bescheid</button></td></tr>').join('')+'</tbody></table></div>')+
|
||||
'</div>'+
|
||||
'<div class="card"><div class="ch"><span class="ct">Aufgaben</span></div>'+
|
||||
@@ -1538,6 +1570,7 @@ async function loadPat(id){
|
||||
'<button class="btn" style="margin-left:auto;color:var(--amtx);border-color:var(--amtx)" onclick="archivierenModal('+id+',\''+p.vorname+' '+p.nachname+'\')"><i class="ti ti-archive"></i>Archivieren</button>'+
|
||||
'</div>',
|
||||
p.vorname+' '+p.nachname);
|
||||
rtInit();
|
||||
// Tagebuch-Vorschau laden
|
||||
api('/api/patienten/'+id+'/tagebuch').then(entries=>{
|
||||
const el=document.getElementById('tbsummary');if(!el)return;
|
||||
@@ -2553,12 +2586,13 @@ async function pgAufgaben(){
|
||||
'<button class="btn btn-sm" onclick="newAM()"><i class="ti ti-plus"></i>Neue Aufgabe</button></div>'+
|
||||
(data.length===0?'<p class="muted sm">Keine offenen Aufgaben – alles erledigt!</p>':
|
||||
'<div class="tw rtable"><table><thead><tr><th>Aufgabe</th><th>Patient</th><th>Fällig</th><th>Priorität</th><th></th></tr></thead><tbody>'+
|
||||
data.map(a=>'<tr><td data-label="Aufgabe"><div class="w5">'+a.titel+'</div>'+(a.beschreibung?'<div class="sm muted">'+a.beschreibung+'</div>':'')+
|
||||
data.map(a=>'<tr><td data-label="Aufgabe" class="rt-head"><div><div class="w5">'+a.titel+'</div>'+(a.beschreibung?'<div class="sm muted">'+a.beschreibung+'</div>':'')+'</div><i class="ti ti-chevron-down rt-chev"></i>'+
|
||||
'</td><td data-label="Patient">'+(a.patient_name?'<span class="tl" onclick="loadPat('+a.patient_id+')">'+a.patient_name+'</span>':'<span class="muted">–</span>')+
|
||||
'</td><td data-label="Fällig">'+fdate(a.faellig_am)+'</td><td data-label="Priorität">'+prio(a.prioritaet)+
|
||||
'</td><td><button class="btn btn-sm btn-p" onclick="erlA('+a.id+')"><i class="ti ti-check"></i>Erledigt</button></td></tr>').join('')+
|
||||
'</tbody></table></div>')+
|
||||
'</div>','Offene Aufgaben');
|
||||
rtInit();
|
||||
}
|
||||
async function erlA(id){
|
||||
await api('/api/aufgaben/'+id+'/erledigt',{method:'POST'});
|
||||
|
||||
Reference in new issue
Block a user