Sanfte Klapp-Animation, gestaffelte Interview-Fragen, Antragsdatum-Wheel

- Akkordeon-Karten (rtable) animieren jetzt sanft statt abrupt:
  display:none/block durch max-height/opacity/padding-Transition
  ersetzt (display ist nicht transition-faehig, das war die Ursache
  des "Hau-Ruck"-Effekts). Per Mid-Transition-Screenshot verifiziert.
- Interview Step 4 (Pflegealltag-Fragen) zeigt jetzt eine Frage nach
  der anderen statt einer langen Liste: neuWiz.q4sub trackt die
  Position, eigener duenner Fortschrittsbalken (.subdots) innerhalb
  des Schritts. Antwort-Klick rueckt automatisch zur naechsten Frage
  vor, ausser bei der letzten (dort erst per bewusstem "Weiter").
  Zurueck von Schritt 5 landet korrekt auf der letzten Unterfrage.
- openGebDatumPicker generalisiert zu openDatePicker(hiddenId,dispId,
  {fallback,titel,icon,yearMin,yearMax}) und beim Antragsdatum (#adt)
  im Antrag-Pflegegrad-Formular eingesetzt (Default-Scrollposition:
  heute, nicht heute-75 Jahre wie beim Geburtsdatum - fuer nahe
  Termine ist "heute" die sinnvollere Startposition)

Verifiziert per Playwright: alle 6 Unterfragen in Step 4 einzeln
durchgeklickt (inkl. Stopp vor Step 5 bei der letzten Frage und
korrekte Zurueck-Navigation), Antragsdatum-Picker mit korrektem Titel/
Icon/Default getestet, Klapp-Animation per Mid-Transition-Screenshot
bestaetigt.
This commit is contained in:
admin committed 2026-07-18 13:33:43 +02:00
1 parent 7cb6191e83
commit 8a2e338e9d
2 files changed
+88 -42

No files matched your search

+3 -2
View File
@@ -69,8 +69,9 @@ 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** its Audit-Log (originally a fixed-min-width flex list — converted to an `.rtable` specifically because flex rows with `min-width` don't degrade gracefully, unlike this pattern), and the Patientenakte Anträge table. Follow this pattern for any new data table — don't ship a raw `<table>` (or a fixed-width flex row list) 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.
- **Birth-date fields use a scroll-wheel picker, not native `<input type="date">`**: for a care app, birth years are routinely 60-100 years in the past, and a native calendar widget forces endless "previous month" clicking. `openGebDatumPicker(hiddenId, dispId)` opens an `OM()` modal with three CSS-scroll-snap columns (Tag/Monat/Jahr, `.dwheel`/`.dwheel-col`/`.dwheel-item`, `DW_ITEM_H=38`); the centered item per column is read on confirm (`dwConfirm()`), clamped for invalid day/month combos, and written back as an ISO string. The pattern is: a visible `.dw-trigger` div (click opens the picker, shows the formatted date via `dwTriggerLabel(hiddenId, dispId)`) plus a paired `<input type="hidden">` that holds the real `YYYY-MM-DD` value everything else in the codebase already expects — so `neuCollect()`/`fillWP()`/`fillM()`/`updM()` etc. needed no changes beyond calling `dwTriggerLabel()` after programmatically setting the hidden input (e.g. when a patient is selected and auto-fills the form). Applied only to actual birth-date fields — `fgeb` (interview wizard), `wgeb` (Widerspruch-Generator), `mpgeb` (Musterbrief) — **not** to near-term admin dates (Antragsdatum, Bescheiddatum, Fällig-am, etc.), which stay native `<input type="date">` since a normal calendar is faster for dates within the last/next few months. Defaults to *today − 75 years* rather than today, since that's a much better starting scroll position for this app's typical patient age. If you wire the picker to a field with existing live-update behavior (Musterbrief's `updM()` was previously triggered via `oninput`), you must call that update function explicitly from `dwConfirm()` — switching to `type="hidden"` removes the inline `oninput` handler, and events dispatched on a hidden input don't automatically reach anything unless something explicitly listens for them.
- **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. **Expand/collapse animates** via `max-height`/`opacity`/`padding` transitions on each `td[data-label]` (a fixed `max-height:130px` ceiling, not `height:auto`, since `display` and `height:auto` aren't transition-able) — don't revert this to a `display:none`/`block` toggle, that's what made it feel instant/abrupt before.
- **Date-wheel picker, not native `<input type="date">`**, for any date field where users need to scroll far from today: `openDatePicker(hiddenId, dispId, {fallback, titel, icon, yearMin, yearMax})` opens an `OM()` modal with three CSS-scroll-snap columns (Tag/Monat/Jahr, `.dwheel`/`.dwheel-col`/`.dwheel-item`, `DW_ITEM_H=38`); the centered item per column is read on confirm (`dwConfirm()`), clamped for invalid day/month combos, and written back as an ISO string. The pattern is: a visible `.dw-trigger` div (click opens the picker, shows the formatted date via `dwTriggerLabel(hiddenId, dispId, icon)`) plus a paired `<input type="hidden">` that holds the real `YYYY-MM-DD` value everything else in the codebase already expects — so `neuCollect()`/`fillWP()`/`fillM()`/`updM()` etc. needed no changes beyond calling `dwTriggerLabel()` after programmatically setting the hidden input (e.g. when a patient is selected and auto-fills the form). `openGebDatumPicker(hiddenId, dispId)` is a thin wrapper around `openDatePicker` for birth dates specifically: `icon:'ti-cake'`, `fallback` = *today − 75 years* (a far better scroll starting point than today, given this app's typical patient age) — used on `fgeb` (interview wizard), `wgeb` (Widerspruch-Generator), `mpgeb` (Musterbrief). `openDatePicker` is called directly (no wrapper, default fallback = today, `icon:'ti-calendar'`) for near-term dates where "today" is already the right starting point, e.g. `adt` (Antragsdatum). Dates that are *always* within the last/next few months and don't need this at all (Bescheiddatum, Fällig-am, Tagebuch/Dokument-Datum, etc.) can stay a plain `<input type="date">` — only convert a field once someone actually needs to scroll years back from it. If you wire the picker to a field with existing live-update behavior (Musterbrief's `updM()` was previously triggered via `oninput`), you must call that update function explicitly from `dwConfirm()` — switching to `type="hidden"` removes the inline `oninput` handler, and events dispatched on a hidden input don't automatically reach anything unless something explicitly listens for them.
- **Interview wizard step 4 asks one question at a time**, not a long scrolling list: `neuWiz.q4sub` (0 = Zielgruppe, 1-5 = each `NEU_FRAGEN` entry) tracks position within the step; `NEU_Q4_TOTAL` is the count. A thin segmented progress bar (`.subdots`/`.subdot`, distinct from the big numbered `.wiz-steps` circles for the outer 6 steps) shows position within step 4 specifically. Answering a question (`neuAntwort`/`neuSetZG`) auto-advances `q4sub` to the next one — except on the last question, which waits for an explicit "Weiter" tap so the user gets a moment before jumping to the estimate in step 5. `neuNext()`/`neuBack()` both special-case `step===4`: they move `q4sub` within the step before ever touching the outer `step` counter, and entering step 4 from step 5 via "Zurück" lands on the *last* sub-question (`q4sub=NEU_Q4_TOTAL-1`), matching normal back-navigation expectations. If you add a 7th situational question, nothing else needs to change — `NEU_Q4_TOTAL` and the subdots derive from `NEU_FRAGEN.length` automatically.
- **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.
- **Dashboard quick-actions become a sticky bottom bar on phones**: below 560px, `.bento-quick` (the "Schnellzugriff" tile) switches from a normal bento grid cell to `position:fixed;bottom:0`, a horizontal icon-only tab bar (`.bq-hd` heading hidden, `.bq-list` buttons go icon-over-2-line-label via `-webkit-line-clamp:2`), so the primary actions never require scrolling to reach. `.bento{padding-bottom:80px}` in that same breakpoint reserves the space the fixed bar would otherwise cover — this is scoped to `.bento` specifically (not a global `#content` padding) since `.bento-quick`/`.bento` only ever exist on the Dashboard page.
- **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.
@@ -167,21 +167,22 @@ body{
@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:6px;padding:10px 12px;box-shadow:var(--glass-shadow);cursor:pointer}
.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;transition:box-shadow .25s ease,border-color .25s ease,padding-bottom .25s ease}
.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{border:none;padding:8px 0}
.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}
.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;transition:opacity .2s ease}
/* 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}
die Kopfzeile zusammengeklappt und stapelt sich eng - weniger Scrollen.
max-height statt display:none, damit das Auf-/Zuklappen sanft animiert
(display ist nicht transition-fähig). */
.rtable tr td[data-label]{max-height:0;opacity:0;overflow:hidden;padding-top:0;padding-bottom:0;border-bottom:1px solid transparent;transition:max-height .32s ease,opacity .22s ease,padding .32s ease,border-color .32s ease}
.rtable tr td.rt-head{max-height:none;opacity:1;overflow:visible;display:flex!important;align-items:center;justify-content:space-between;gap:8px;border-bottom:none!important;padding:0!important}
.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-chev{display:inline-block;font-size:16px;color:var(--hint);transition:transform .25s ease;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 td[data-label]{max-height:130px;opacity:1;padding-top:8px;padding-bottom:8px;border-bottom-color:var(--border)}
.rtable tr.rt-open td.rt-head{border-bottom:1px solid var(--border)!important;padding:0 0 8px!important;margin-bottom:6px}
.rtable tr.rt-open .rt-chev{transform:rotate(180deg);color:var(--green)}
}
table{width:100%;border-collapse:collapse}
@@ -256,6 +257,9 @@ textarea{resize:vertical;min-height:80px}
.wiz-line{flex:1;height:1px;background:var(--border)}
.wiz-title{font-size:16px;font-weight:500;margin-bottom:4px}
.wiz-hint{font-size:12.5px;color:var(--muted);margin-bottom:16px;line-height:1.5}
.subdots{display:flex;gap:5px;margin-bottom:18px}
.subdot{flex:1;height:4px;border-radius:2px;background:var(--border2);transition:background .25s ease}
.subdot.done{background:var(--green)}
.ans-pill{display:flex;align-items:flex-start;gap:10px;padding:11px 14px;border-radius:var(--r);border:1px solid var(--border2);cursor:pointer;font-size:13.5px;color:var(--text);margin-bottom:8px;transition:all .12s}
.ans-pill:hover{background:var(--bg)}
.ans-pill.on{border-color:var(--green);background:var(--gbg);color:var(--gdk);font-weight:500}
@@ -508,26 +512,33 @@ function rtInit(){
});
});
}
// ── Geburtsdatum-Wheel-Picker (Tag/Monat/Jahr-Scroll-Spalten) ───────────────
// ── Datums-Wheel-Picker (Tag/Monat/Jahr-Scroll-Spalten) ─────────────────────
// Generisch fuer jedes Datumsfeld nutzbar (Geburtsdatum, Antragsdatum, ...) -
// unterscheidet sich pro Feld nur durch Titel/Icon/Default-Scrollposition.
const DW_MONATE=['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
const DW_ITEM_H=38;
let _dwHidden=null,_dwDisp=null;
function dwTriggerLabel(hiddenId,dispId){
let _dwHidden=null,_dwDisp=null,_dwIcon='ti-calendar';
function dwTriggerLabel(hiddenId,dispId,icon){
const el=document.getElementById(dispId);
const hid=document.getElementById(hiddenId);
if(!el)return;
el.innerHTML=hid&&hid.value?'<i class="ti ti-cake"></i>'+fdate(hid.value):'<i class="ti ti-cake"></i><span class="dw-ph">Datum wählen…</span>';
icon=icon||'ti-calendar';
el.innerHTML=hid&&hid.value?'<i class="ti '+icon+'"></i>'+fdate(hid.value):'<i class="ti '+icon+'"></i><span class="dw-ph">Datum wählen…</span>';
}
function openGebDatumPicker(hiddenId,dispId){
_dwHidden=hiddenId;_dwDisp=dispId;
function openDatePicker(hiddenId,dispId,opts){
opts=opts||{};
_dwHidden=hiddenId;_dwDisp=dispId;_dwIcon=opts.icon||'ti-calendar';
const cur=document.getElementById(hiddenId)?.value||'';
const heute=new Date();
let d=heute.getDate(),m=heute.getMonth()+1,y=heute.getFullYear()-75; // sinnvoller Default für Pflege-Patienten statt "heute"
let d=heute.getDate(),m=heute.getMonth()+1,y=heute.getFullYear();
if(opts.fallback){const fp=opts.fallback.split('-');if(fp.length===3){y=parseInt(fp[0]);m=parseInt(fp[1]);d=parseInt(fp[2]);}}
if(cur){const parts=cur.split('-');if(parts.length===3){y=parseInt(parts[0]);m=parseInt(parts[1]);d=parseInt(parts[2]);}}
const dayItems=Array.from({length:31},(_,i)=>({v:i+1,label:String(i+1)}));
const monthItems=DW_MONATE.map((n,i)=>({v:i+1,label:n}));
const yearItems=[];for(let yy=heute.getFullYear();yy>=1900;yy--)yearItems.push({v:yy,label:String(yy)});
OM('Geburtsdatum wählen',
const yMax=opts.yearMax||heute.getFullYear()+1;
const yMin=opts.yearMin||1900;
const yearItems=[];for(let yy=yMax;yy>=yMin;yy--)yearItems.push({v:yy,label:String(yy)});
OM(opts.titel||'Datum wählen',
'<div class="dwheel">'+
'<div class="dwheel-col" id="dw-day"></div>'+
'<div class="dwheel-col" id="dw-month"></div>'+
@@ -563,6 +574,11 @@ function openGebDatumPicker(hiddenId,dispId){
};
});
}
function openGebDatumPicker(hiddenId,dispId){
const heute=new Date();
const fallback=(heute.getFullYear()-75)+'-'+String(heute.getMonth()+1).padStart(2,'0')+'-'+String(heute.getDate()).padStart(2,'0');
openDatePicker(hiddenId,dispId,{fallback,titel:'Geburtsdatum wählen',icon:'ti-cake'});
}
function dwBuildCol(id,items){
const col=document.getElementById(id);
const pad=(190-DW_ITEM_H)/2;
@@ -589,7 +605,7 @@ function dwConfirm(){
const iso=y+'-'+String(m).padStart(2,'0')+'-'+String(d).padStart(2,'0');
const hid=document.getElementById(_dwHidden);
if(hid){hid.value=iso;hid.dispatchEvent(new Event('change'));}
dwTriggerLabel(_dwHidden,_dwDisp);
dwTriggerLabel(_dwHidden,_dwDisp,_dwIcon);
if(_dwHidden==='mpgeb'&&typeof updM==='function')updM(); // Musterbrief-Vorschau live nachziehen
CM();
}
@@ -1759,6 +1775,7 @@ const NEU_ZG_OPTIONEN=[
{label:'Behinderung (angeboren oder erworben)',zg:'behinderte',hinweis:'z. B. körperliche, geistige oder seelische Behinderung'},
{label:'Andere Bedürftigkeit oder soziale Notlage',zg:'beduerftige',hinweis:'z. B. vorübergehende Notlage ohne dauerhafte Behinderung'},
];
const NEU_Q4_TOTAL=1+NEU_FRAGEN.length; // Zielgruppe + 5 Situationsfragen, einzeln gestaffelt
function neuSchaetzung(){
const a=neuWiz.antworten;
const summe=NEU_FRAGEN.reduce((s,f)=>s+(a[f.key]??0),0);
@@ -1772,10 +1789,10 @@ function neuSchaetzung(){
return {summe,von,bis,vorschlag:Math.round((von+bis)/2)};
}
let neuWiz={step:1,data:{},antworten:{},zielgruppe:'alterspflege'};
let neuWiz={step:1,data:{},antworten:{},zielgruppe:'alterspflege',q4sub:0};
function pgNeu(){
neuWiz={step:1,data:{},antworten:{},zielgruppe:'alterspflege'};
neuWiz={step:1,data:{},antworten:{},zielgruppe:'alterspflege',q4sub:0};
neuRender();
}
function neuSteps(){return ['Persönliche Daten','Kontakt & Adresse','Krankenversicherung','Pflegesituation','Einschätzung & Pflegeperson','Übersicht'];}
@@ -1799,15 +1816,34 @@ function neuCollect(){
Object.assign(neuWiz.data,{pflegeperson:g('fpp'),notizen:g('fnotiz'),pflegegrad:g('fgrad')});
}
}
function neuAntwort(key,wert){neuWiz.antworten[key]=wert;neuRender();}
function neuSetZG(zg){neuWiz.zielgruppe=zg;neuRender();}
function neuBack(){neuCollect();if(neuWiz.step>1){neuWiz.step--;neuRender();}}
function neuQ4Advance(){
// Nach einer Antwort automatisch zur naechsten Frage - ausser bei der
// letzten, da rueckt man bewusst erst per "Weiter" weiter (kurzer Moment
// zum Nachdenken/Korrigieren statt sofortigem Sprung zu Schritt 5).
if(neuWiz.q4sub<NEU_Q4_TOTAL-1)neuWiz.q4sub++;
}
function neuAntwort(key,wert){neuWiz.antworten[key]=wert;neuQ4Advance();neuRender();}
function neuSetZG(zg){neuWiz.zielgruppe=zg;neuQ4Advance();neuRender();}
function neuBack(){
neuCollect();
if(neuWiz.step===4&&neuWiz.q4sub>0){neuWiz.q4sub--;neuRender();return;}
if(neuWiz.step>1){
neuWiz.step--;
if(neuWiz.step===4)neuWiz.q4sub=NEU_Q4_TOTAL-1; // von Schritt 5 zurueck: letzte Frage zeigen
neuRender();
}
}
function neuNext(){
neuCollect();
if(neuWiz.step===1&&(!neuWiz.data.vorname||!neuWiz.data.nachname)){
toast('Bitte Vor- und Nachname eintragen',false);return;
}
if(neuWiz.step<neuSteps().length){neuWiz.step++;neuRender();}
if(neuWiz.step===4&&neuWiz.q4sub<NEU_Q4_TOTAL-1){neuWiz.q4sub++;neuRender();return;}
if(neuWiz.step<neuSteps().length){
neuWiz.step++;
if(neuWiz.step===4)neuWiz.q4sub=0; // frisch von Schritt 3 rein: erste Frage zeigen
neuRender();
}
}
function neuRender(){
const step=neuWiz.step,d=neuWiz.data;
@@ -1841,17 +1877,24 @@ function neuRender(){
<div class="fgrp"><label>Versicherungsart</label><select id="fvart"><option value="GKV" ${d.versicherungsart!=='PKV'?'selected':''}>Gesetzlich (GKV)</option><option value="PKV" ${d.versicherungsart==='PKV'?'selected':''}>Privat (PKV)</option></select></div>
</div>`;
}else if(step===4){
body=`<div class="wiz-title">Was beschreibt die Situation am besten?</div>
<div class="wiz-hint">Das hilft uns, passende Leistungen vorzuschlagen. Sie können das später jederzeit ändern.</div>
<div class="mb4">`+NEU_ZG_OPTIONEN.map(o=>
`<div class="ans-pill ${neuWiz.zielgruppe===o.zg?'on':''}" onclick="neuSetZG('${o.zg}')"><div class="dot"></div><div><div>${o.label}</div><div class="muted sm">${o.hinweis}</div></div></div>`
).join('')+`</div>
<div class="wiz-title" style="margin-top:20px">Wie sieht der Pflegealltag aus?</div>
<div class="wiz-hint">Diese Fragen helfen bei einer <strong>ersten groben Einschätzung</strong>. Sie ersetzen nicht die offizielle Begutachtung durch den Medizinischen Dienst.</div>`+
NEU_FRAGEN.map(f=>`<div class="mb4">
<div class="mb3" style="font-size:13px;font-weight:500">${f.frage}</div>
${f.optionen.map(o=>`<div class="ans-pill ${neuWiz.antworten[f.key]===o.wert?'on':''}" onclick="neuAntwort('${f.key}',${o.wert})"><div class="dot"></div><div>${o.label}</div></div>`).join('')}
</div>`).join('');
// Eine Frage nach der anderen statt einer langen Liste - besser lesbar,
// und die letzte Frage ist so immer ohne Scrollen erreichbar.
const subdots='<div class="subdots">'+Array.from({length:NEU_Q4_TOTAL},(_,i)=>
'<div class="subdot '+(i<=neuWiz.q4sub?'done':'')+'"></div>').join('')+'</div>';
if(neuWiz.q4sub===0){
body=subdots+`<div class="wiz-title">Was beschreibt die Situation am besten?</div>
<div class="wiz-hint">Das hilft uns, passende Leistungen vorzuschlagen. Sie können das später jederzeit ändern.</div>
<div class="mb4">`+NEU_ZG_OPTIONEN.map(o=>
`<div class="ans-pill ${neuWiz.zielgruppe===o.zg?'on':''}" onclick="neuSetZG('${o.zg}')"><div class="dot"></div><div><div>${o.label}</div><div class="muted sm">${o.hinweis}</div></div></div>`
).join('')+`</div>`;
}else{
const f=NEU_FRAGEN[neuWiz.q4sub-1];
body=subdots+`<div class="wiz-hint" style="margin-bottom:10px">Diese Fragen helfen bei einer <strong>ersten groben Einschätzung</strong> des Pflegealltags. Sie ersetzen nicht die offizielle Begutachtung durch den Medizinischen Dienst.</div>
<div class="wiz-title">${f.frage}</div>
<div class="mb4">`+f.optionen.map(o=>
`<div class="ans-pill ${neuWiz.antworten[f.key]===o.wert?'on':''}" onclick="neuAntwort('${f.key}',${o.wert})"><div class="dot"></div><div>${o.label}</div></div>`
).join('')+`</div>`;
}
}else if(step===5){
const est=neuSchaetzung();
body=`<div class="estimate-box">
@@ -2000,7 +2043,9 @@ async function pgAntrag(prefillPid){
'reha:Reha-Antrag (§ 40 SGB V)','eingliederung:Eingliederungshilfe (SGB IX)',
'grundsicherung:Grundsicherung (§ 41 SGB XII)'].map(x=>{const[v,l]=x.split(':');return'<option value="'+v+'">'+l+'</option>';}).join('')+'</select></div>'+
'<div class="fg c2">'+
'<div class="fgrp"><label>Antragsdatum</label><input type="date" id="adt"></div>'+
'<div class="fgrp"><label>Antragsdatum</label>'+
'<div class="dw-trigger" id="adt_disp" onclick="openDatePicker(\'adt\',\'adt_disp\',{titel:\'Antragsdatum wählen\'})"><i class="ti ti-calendar"></i><span class="dw-ph">Datum wählen…</span></div>'+
'<input type="hidden" id="adt"></div>'+
'<div class="fgrp"><label>Beantragter Pflegegrad</label><select id="agrad">'+[1,2,3,4,5].map(g=>'<option value="'+g+'">Grad '+g+'</option>').join('')+'</select></div>'+
'</div><div class="fgrp"><label>Notizen</label><textarea id="anotiz" rows="2"></textarea></div></div>'+
'<div class="fl g8" style="justify-content:flex-end"><button class="btn btn-p" onclick="saveA()"><i class="ti ti-check"></i>Antrag anlegen</button></div></div>'+
@@ -2227,7 +2272,7 @@ async function fillWP(){
const pid=document.getElementById('wpid')?.value;if(!pid)return;
const d=await api('/api/patienten/'+pid);const p=d.patient;
const sv=(id,v)=>{const e=document.getElementById(id);if(e&&v)e.value=v;};
sv('wname',p.vorname+' '+p.nachname);sv('wgeb',p.geburtsdatum);dwTriggerLabel('wgeb','wgeb_disp');sv('wvnr',p.versichertennummer);
sv('wname',p.vorname+' '+p.nachname);sv('wgeb',p.geburtsdatum);dwTriggerLabel('wgeb','wgeb_disp','ti-cake');sv('wvnr',p.versichertennummer);
sv('waddr',(p.strasse?p.strasse+', ':'')+p.plz+' '+p.ort);
if(p.krankenkasse)selKK('wkk_s','wkk',p.krankenkasse);
if(p.pflegegrad)sv('wist',p.pflegegrad);
@@ -2460,7 +2505,7 @@ async function fillM(pid){
const sv=(id,v)=>{const e=document.getElementById(id);if(e&&v!==undefined)e.value=v;};
sv('mn',p.vorname+' '+p.nachname);sv('ms',p.strasse||'');sv('mpo',(p.plz||'')+' '+(p.ort||''));
sv('mtel',p.telefon||'');sv('mpn',p.vorname+' '+p.nachname);sv('mpgeb',p.geburtsdatum||'');sv('mpvnr',p.versichertennummer||'');
dwTriggerLabel('mpgeb','mpgeb_disp');
dwTriggerLabel('mpgeb','mpgeb_disp','ti-cake');
if(p.krankenkasse)selKK('mkk_s','mkk',p.krankenkasse);
updM();
}