diff --git a/CLAUDE.md b/CLAUDE.md index 63a52eb..fa65a88 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 ``. Below 700px, `.rtable` CSS turns each `` 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 `` 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 `
` 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. diff --git a/pflegepilot_alpha_v2/pflegepilot/templates/index.html b/pflegepilot_alpha_v2/pflegepilot/templates/index.html index 19b0c2b..61a02a5 100644 --- a/pflegepilot_alpha_v2/pflegepilot/templates/index.html +++ b/pflegepilot_alpha_v2/pflegepilot/templates/index.html @@ -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=''+m;document.body.appendChild(t);setTimeout(()=>t.remove(),2800);} function escH(s){return(s||'').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 +// -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){ :'kein Zugang'); return ''+ ''+ - ''+ + ''+ ''+ ''+ ''+ @@ -1300,6 +1329,7 @@ function renderAdminDash(nutzer,log,patienten){ logRows+ '', '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(){ '
Aktuelle Patienten
'+ '
'+av(n.name||n.benutzername,n.id)+'
'+escH(n.name||n.benutzername)+'
@'+escH(n.benutzername)+'
'+escH(n.name||n.benutzername)+'
@'+escH(n.benutzername)+'
'+(n.rolle==='admin'?'Admin':'Nutzer')+''+(gesperrt?'Gesperrt':'Aktiv')+''+n.patienten_anzahl+'
'+ - d.recent.map((p,i)=>'').join('')+ '
NameGradZielgruppeStatus
'+av(p.vorname+' '+p.nachname,i)+''+p.vorname+' '+p.nachname+'
'+ + d.recent.map((p,i)=>'
'+av(p.vorname+' '+p.nachname,i)+''+p.vorname+' '+p.nachname+'
'+ (p.pflegegrad?'Grad '+p.pflegegrad+'':'–')+''+zg_badge(p.zielgruppe||'alterspflege')+''+bdg(p.status||'offen')+'
'+ @@ -1403,6 +1433,7 @@ async function pgDash(){ 'Dashboard', ''); 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 ''+ - '
'+av(p.vorname+' '+p.nachname,i)+ + '
'+av(p.vorname+' '+p.nachname,i)+ '
'+p.vorname+' '+p.nachname+''+ (archiv?''+(stLabel[p.patient_status]||p.patient_status)+'':'')+ - '
'+ + '
'+ ''+fdate(p.geburtsdatum)+''+ ''+(p.krankenkasse||'–')+''+ ''+(p.pflegegrad?'Grad '+p.pflegegrad+'':'–')+''+ @@ -1439,6 +1470,7 @@ function renderPT(data){ ''; }).join('')+ ''); + rtInit(); } async function filterPat(){ let url='/api/patienten?'; @@ -1502,7 +1534,7 @@ async function loadPat(id){ '
Anträge
'+ (d.antraege.length===0?'

Noch kein Antrag.

': '
'+ - d.antraege.map(a=>''+ + d.antraege.map(a=>''+ '').join('')+'
TypDatumGradStatusDokument
'+a.typ+''+fdate(a.antragsdatum)+''+(a.beantragter_pflegegrad?'Grad '+a.beantragter_pflegegrad:'–')+''+bdg(a.status)+''+(a.pdf_pfad?' PDF öffnen':'–')+'
'+a.typ+''+fdate(a.antragsdatum)+''+(a.beantragter_pflegegrad?'Grad '+a.beantragter_pflegegrad:'–')+''+bdg(a.status)+''+(a.pdf_pfad?' PDF öffnen':'–')+'
')+ '
'+ '
Aufgaben
'+ @@ -1538,6 +1570,7 @@ async function loadPat(id){ ''+ '
', 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(){ ''+ (data.length===0?'

Keine offenen Aufgaben – alles erledigt!

': '
'+ - data.map(a=>'').join('')+ '
AufgabePatientFälligPriorität
'+a.titel+'
'+(a.beschreibung?'
'+a.beschreibung+'
':'')+ + data.map(a=>'
'+a.titel+'
'+(a.beschreibung?'
'+a.beschreibung+'
':'')+'
'+ '
'+(a.patient_name?''+a.patient_name+'':'–')+ ''+fdate(a.faellig_am)+''+prio(a.prioritaet)+ '
')+ '','Offene Aufgaben'); + rtInit(); } async function erlA(id){ await api('/api/aufgaben/'+id+'/erledigt',{method:'POST'});