tagesdokumentation_erfassung.html hat im Footer eine Versionsnummer
(„vX.Y · Alexander Zabelyshenskiy”), identisch in build_html.py gepflegt.
Bei jeder inhaltlichen Änderung an HTML/JS (nicht bei reinen Doku-Änderungen)
die Versionsnummer in BEIDEN Dateien hochzählen (Bugfix → Patch, z.B. 1.5→1.6;
neues Feature → Minor, z.B. 1.6→1.7).
Dr. Alexander Zabelyshenskiy (Oberarzt, Allgemein-, Viszeral- und Gefäßchirurgie, Klinikum Landkreis Freudenstadt) muss täglich einen offiziellen Klinikum-Bogen (“Tagesdokumentation: Überstunden und Inanspruchnahme im Rufdienst”) ausfüllen. Das Original-PDF-Layout/der Text darf nicht verändert werden (offizielles Dokument, Weiterleitung an Personalabteilung + Chefarzt-Unterschrift).
Tagesdokumentation_Ueberstunden_Rufdienst_ausfuellbar.pdf):
Original-PDF + echte, interaktive AcroForm-Textfelder (keine reinen Text-Overlays).
tagesdokumentation_erfassung.html):
Eigenständige HTML-Datei mit eingebettetem Base64-PDF-Template + pdf-lib
(CDN). Erfasst Tageswerte komfortabel im Browser, berechnet automatisch
(Überstunden, Dauer je Rufdienst-Zeile, Summe, Anzahl Einsätze) und füllt
beim Klick auf “PDF ausfüllen & herunterladen” die echten PDF-Formularfelder
aus (per pdf-lib form.getTextField(id).setText(...)), dann Download.
amb2: Mo, 07:10–16:05amb4: 07:25–16:05ZD1: Mo–Do, 11:00–18:00ZD2: 11:00–18:00RBD: nur Sa, 09:00–10:30 (danach zählt als RBA → Hinweis in Erläuterung)RBD2: nur So/Feiertag, Arbeitszeit wie SaFRN (frei nach Dienst, nach Nachtoperation): Checkbox setzt Bemerkungstext
“FRN – frei nach Dienst” in Erläuterung. Normale Arbeitszeit wird trotzdem
eingetragen (kein Minusstunden-Abzug) – das ist Absicht, nicht korrigieren!RBA = Aktivzeit (von/bis), RBT = Telefonat (von/bis) – das sind KEINE
Dienstart-Optionen, sondern beschreiben Einsatzarten im Rufdienst
(Abschnitt 2). Das Dropdown “Art der Inanspruchnahme” nutzt aber den
offiziellen Formular-Wortlaut (telefonisch/Präsenz im Haus/Operation/Sonstiges).TextStringObject escaped bei /DA-Strings jedes
Nicht-alphanumerische Zeichen als Oktal (/ → \057). pdf-lib kann das beim
Parsen des DA-Strings nicht auflösen (“No Tf operator found”) bzw. rendert
dann mit falscher/riesiger Schriftgröße. Fix: /DA als ByteStringObject
(Hex-String) statt TextStringObject schreiben./DR (Default Resources) mit Font-Ressource /Helv,
sonst kann pdf-lib setFontSize() nicht anwenden./BS (Border Style, W:0) bei editierbaren Feldern: Edge und
PDF24 (beide nutzen die Rendering-Engine “pdfium”) zeichnen dann einen
sichtbaren Standard-Rahmen um jedes Feld, der versetzt zu den Tabellenlinien
liegt (“alles verschoben”). Poppler zeigt diesen Fehler NICHT an – bei
Sichtprüfung also nicht nur mit poppler/pdf2image testen, sondern wenn
möglich auch mit einem pdfium-basierten Viewer. Fix: /BS {W:0} auf
JEDES Feld setzen (nicht nur readonly-Felder), plus leeres /MK.page.get_drawings() (PyMuPDF) nutzen, um die echten Fill-Rechtecke
und Trennlinien der Vorlage auszulesen, und Feldkoordinaten exakt daran
ausrichten. Beispiel-Workflow:
import fitz
doc = fitz.open(SRC)
page = doc[0]
for p in page.get_drawings():
r = p['rect']
print(r.x0, r.y0, r.x1, r.y1, 'FILL' if p.get('fill') else 'LINE')
Damit lassen sich Zellgrenzen (farbige Fill-Rechtecke) UND Trennlinien
(schmale Linien-Rechtecke) exakt bestimmen – zuverlässiger als
extract_form_structure.py-Zeilenschätzungen oder Label-Textpositionen.
extract_form_structure.py bzw. page.search_for() (PyMuPDF) nur als
groben Anhaltspunkt nehmen, NICHT als finale Wahrheit – siehe oben.add_fields.py: top-down (y=0 oben), Konvertierung nach
PDF-Standard (y=0 unten) über top_rect_to_pdf(), PAGE_H = 841.89./Widget-Annotation-Dictionaries gebaut (siehe add_fields.py).node + pdf-lib testweise Felder befüllen
und per pdf2image rendern (siehe test_final2.pdf-Workflow im Verlauf),
UND den Nutzer explizit bitten, in Edge/PDF24 zu prüfen (pdfium-Rendering
weicht von poppler ab).Alle Feldkoordinaten wurden gegen die exakten Vektor-Zellgrenzen der
Original-PDF geprüft (Kopfteil, Abschnitt 1, 2, 3). Kein bekannter
Layout-Bug mehr offen. Bei künftigen Änderungen an Koordinaten immer
page.get_drawings() verwenden (siehe oben), nicht schätzen.
add_fields.py – Python-Skript: nimmt Original-PDF, fügt 67 AcroForm-Felder
hinzu, schreibt Tagesdokumentation_Ueberstunden_Rufdienst_ausfuellbar.pdfbuild_html.py – Python-Skript: baut tagesdokumentation_erfassung.html
(bettet die ausgefüllte PDF als Base64 ein)Tagesdokumentation_Ueberstunden_Rufdienst_ausfuellbar.pdf – fertiges Ergebnistagesdokumentation_erfassung.html – fertiges ErgebnisTagesdokumentation_Original.pdf – unverändertes Original (Referenz)ueberstunden_std
(Dienstzeit-Überstunden), keine Verrechnung mit Rufdienst-Summe – ggf. mit
Nutzer klären, ob das so gewünscht istdienstart-Dropdown enthalten:
Regeldienst (Montag) → amb2 (07:10–16:05)Regeldienst → amb4 (07:25–16:05)Rufdienst (Mo–Do) → ZD1 (11:00–18:00)Rufdienst → ZD2 (11:00–18:00)Rufdienst (Samstag) → RBD (09:00–10:30, + Hinweis “RBA in Abschnitt 2 erfassen”)Rufdienst (Sonntag/Feiertag) → RBD2 (wie Samstag)
Auswahl füllt automatisch Dienstbeginn/planmäßiges Dienstende via data-start/data-end
Attributen auf den <option>-Elementen..ro/.rowcalc)
werden bei jedem input/change in einem JSON-Cookie (tagesdoku_formdata,
90 Tage) gespeichert und beim Laden der Seite automatisch wiederhergestellt
(loadFormFromCookie()), inkl. Checkbox-Support (FRN).tagesdokumentation_erfassung.html (Root) – Desktop-Tool, wird per GitHub
Pages unter https://nomiknomik.github.io/tagesdokumentation-ueberstunden-rufdienst/tagesdokumentation_erfassung.html
ausgeliefert. Hat eigene Kopie der Dienstart-Optionsliste (DIENSTART_OPTIONS).app/index.html – PWA-Variante (separates Manifest/Service Worker), hat
EIGENE, unabhängige Kopie der gleichen Logik (DUTY-Objekt mit
amb2/amb4/ZD1/ZD2/RBD/RBD2).
Änderungen an Dienstart-Labels, Zeiten, Minusstunden-Berechnung etc. (also
Logik, die es in BEIDEN Tools gibt) müssen in BEIDEN Dateien parallel
gemacht werden – es gibt keine gemeinsame JS-Datei/kein Include.Ausgangslage war ein Wunsch nach “schönerem Layout”. Verlauf zur Nachvollziehbarkeit für künftige Sessions:
ICONS-
Objekt + icon(name)-Helper in <script>. Statische Platzhalter im
HTML als <span data-icon="clock"></span> werden beim Laden per
document.querySelectorAll('[data-icon]') einmalig durch echtes
<svg> ersetzt (spart, das SVG-Markup in jedem Card-Header zu
duplizieren). Neues Icon hinzufügen = Eintrag in ICONS + data-icon-
Attribut setzen.:root (Farben, Radien,
Spacing) – dadurch ließ sich der komplette spätere Theme-Wechsel
(dunkel → hell) fast nur über die :root-Werte erledigen.expanded-Set in JS,
Reset bei Tageswechsel über lastRenderedDay-Tracking). Alle Felder
(Beginn/Ende/Art/Grund/Fallnummer) bleiben erhalten, nur die Darstellung
ist neu.renderRuhezeitStatus()),
das nur den Wert des bestehenden ja/nein-Dropdowns farbig darstellt –
KEINE neue Berechnungslogik (der offene Punkt “Ruhezeit-Automatik” von
weiter oben ist weiterhin NICHT umgesetzt).header{position:sticky} hat auf dem Gerät nicht zuverlässig oben
fixiert → Statusleiste überlappte Karteninhalt beim Scrollen. Fix:
position:fixed (wie die Bottom-Nav), main-Padding-Top wird per JS
(syncHeaderPad()) anhand der echten Header-Höhe gesetzt, nicht fest
verdrahtet.input[type=time/date] hatte eigenen Rahmen+Hintergrund UND das
native iOS-Zeit-Widget zeigt selbst eine abgerundete Kapsel →
“Doppelkapsel”-Optik. Fix: border-color:transparent;
background:transparent für diese Input-Typen, nur das native Widget
bleibt sichtbar.renderGroupedList() nach Monat gruppiert
(natives <details>/<summary>, kein extra JS-Toggle nötig, CSS
rotiert das Chevron-Icon über .monthgroup[open] summary .ic).:root-Tokens plus:
apple-mobile-web-app-status-bar-style von black-translucent auf
default (sonst wäre die Uhrzeit-Anzeige auf hellem Grund unsichtbar).CACHE-Version in app/sw.js hochzuzählen →
installierte PWA hat trotz gepushtem Code weiter die alte gecachte
index.html ausgeliefert (“online, aber nicht sichtbar”-Verwirrung).
Strukturell gefixt: index.html wird jetzt network-first geladen
(Cache nur Offline-Fallback), alle anderen Assets (PDF, Icons, CDN-Libs)
bleiben cache-first. Dadurch muss die CACHE-Konstante bei reinen
index.html-Änderungen NICHT mehr manuell hochgezählt werden – nur noch,
wenn sich sw.js selbst oder die SHELL-Liste ändert.
Zweiter Teil derselben Falle: Auch mit network-first blieb ein Bugfix
(PDF-Button hinter Bottom-Nav) auf dem Gerät unsichtbar, obwohl der Code
per GitHub-API nachweislich korrekt auf main war – Ursache war der
Service-Worker-Update-Mechanismus selbst: register('sw.js') ohne
explizites reg.update() unterliegt dem Browser-Throttle, und selbst
eine erfolgreich installierte neue SW-Version hat die Seite nicht
automatisch neu geladen. Fix: reg.update() nach der Registrierung
explizit aufrufen, plus location.reload() bei controllerchange bzw.
wenn die neu installierte SW den Status activated erreicht (siehe
Ende von app/index.html, Abschnitt “Start”). Zuverlässigster manueller
Workaround bei Verdacht auf Cache-Trägheit: Home-Screen-Icon löschen und
über Safari neu “Zum Home-Bildschirm hinzufügen” (komplett frische
Installation ohne alten Service Worker).app/index.html hat jetzt APP_VERSION/APP_BUILD (JS-Konstanten am Anfang
des “Start”-Blocks). Angezeigt als Chip im Header (#appVersion) und in der
Einstellungen-Karte “App-Version” (#verNum/#verDate), plus Button
“Nach Update suchen” (#checkUpdate): lädt index.html?cb=… mit
cache:'no-store', vergleicht die APP_VERSION aus dem Quelltext mit der
laufenden und lädt bei Abweichung nach reg.update() neu.
Bei jeder inhaltlichen PWA-Änderung APP_VERSION/APP_BUILD hochzählen –
sonst zeigt die App eine falsche Aktualität an.html,body{height:100%} + padding-bottom
auf body funktionierte auf iOS nicht – bei fester Body-Höhe liegt das
Padding am 100%-Rand, überlaufender Inhalt (PDF-Button, “Diesen Tag
zurücksetzen”) ragte darunter und verschwand hinter der fixen Bottom-Nav.
Fix: nur noch html{height:100%}, unteres Padding auf main
(calc(96px + env(safe-area-inset-bottom))), syncHeaderPad() setzt
main.style.paddingBottom anhand der gemessenen Nav-Höhe (nicht mehr body).#importDataBtn → verstecktes #importData). Liest {cfg, plan, days},
fragt mit Anzahl Plantage/erfasster Tage nach und ersetzt danach plan
und days (cfg wird gemerged), schreibt in localStorage und rendert neu.renderGroupedList() öffnet nicht mehr
automatisch den ersten Monat. Der Auf-/Zuklapp-Zustand wird je Container in
tagesdoku_open_groups (localStorage, openGroups) gemerkt und beim Rendern
wiederhergestellt – überlebt Re-Render und App-Neustart.#icsBtn, buildIcs()). Exportiert genau die Einträge der Karte „Meine
Dienste” (myDutyKeys() – Rufdienste + Abwesenheiten, keine Regeldienste)
als Ganztages-VEVENTs (DTSTART;VALUE=DATE, DTEND = Folgetag), die
Soll-Dienstzeit aus DUTY steht in DESCRIPTION. Ausgabe über
navigator.share({files}) (iOS: Teilen → Kalender), sonst Blob-Download.
UIDs sind stabil (tagesdoku-<datum>-<code>@klf), ein erneuter Import legt
dieselben Termine also nicht doppelt an.DUTY-Labels, ART_OPTS und die neue
GRUND_OPTS in app/index.html entsprechen jetzt exakt
DIENSTART_OPTIONS/artOptions/grundOptions in
tagesdokumentation_erfassung.html (inkl. Kürzel: „Regeldienst (Montag,
AMB2)”, „telefonisch (RBT)”, „Präsenz im Haus (RBA)”, „Operation /
Eingriff”). DUTY kennt zusätzlich FRN, BD, SPD, SONST;
DIENSTART_BY_FUNKTION blendet die Auswahl je Funktion ein (Oberarzt vs.
Assistenzarzt/PA) wie im Desktop-Tool.
Intern bleiben die Codes (ZD1 …) als gespeicherter Wert – nur die Labels
wurden angeglichen, deshalb war keine Datenmigration nötig. Alte
Einsatz-Werte werden über ART_LEGACY/migrateArt() gehoben, abweichende
Altwerte bleiben über optsWith() als Zusatz-Option erhalten.day().dienstart_manuell = true; renderDay() leitet die Dienstart dann
nicht mehr aus dem importierten Plan ab. Unter dem Feld steht ein Hinweis
mit dem Plan-Wert und dem Link „Wieder aus Dienstplan übernehmen”
(#dienstartHint / #dienstartReset).GRUND_OPTS.#btnEinsatz verschmolzen: grün „Einsatz starten”,
nach dem Start rot „Einsatz beenden (läuft seit hh:mm)”
(offenerEinsatz() = letzter Einsatz mit Beginn ohne Ende,
renderEinsatzBtn()). Nachtragen von Hand über den Ghost-Button darunter.
Beginn/Ende/Dauer eines Einsatzes liegen über .grid3 in einer Zeile.renderGroupedList() hat jetzt einen optionalen footerFn-Parameter,
über den renderPlanList() in JEDE Monatsgruppe einen Button
data-ics="<YYYY-MM>" setzt. Ein delegierter Click-Handler auf #planList
filtert myDutyKeys() auf diesen Monat und erzeugt Dienste_<YYYY-MM>.ics.art:'Präsenz im Haus (RBA)' und
grund_anlass:'Notfall / Notoperation' (beides im Formular änderbar).
„Nachtragen” bleibt bewusst leer, „Telefonat” setzt RBT.#btnTelefonat) in der Einsatz-Karte: legt sofort
einen fertigen Einsatz an mit art:'telefonisch (RBT)',
grund_anlass:'Notfall / Notoperation', ende = jetzt, beginn = jetzt
minus 5 Minuten (Dauer 0.08 h). Der Eintrag gilt als abgeschlossen und
bleibt zugeklappt (kein expanded.add); dasselbe beim Beenden eines
laufenden Einsatzes (expanded.delete(i)). Aufgeklappt wird nur der
gerade GESTARTETE Einsatz, damit Fallnummer o.ä. ergänzt werden kann.swap (localStorage: tagesdoku_swap) hält jetzt den Namen des
Diensthabenden je Tag ({"JJJJ-MM-TT": "Nachname"}), nicht mehr eine
Dienstart (v1.11.0-Altwerte werden beim Laden verworfen). derive() legt
den Namen über plan[k].oa und leitet daraus wie gewohnt ab (Wochentag/
Feiertag → ZD1/ZD2/RBD/RBD2), ergänzt um swapped:true und den Zusatz
„· Dienst getauscht” in info. Helfer: oaOf(k).teamRow(), teamOpen): aufgeklappt erscheinen ein Select mit allen im
Plan vorkommenden Namen (bekannteNamen()) plus „– niemand –”, der Button
„Ich übernehme” und – bei bereits getauschten Tagen – „Tausch zurücknehmen”
samt Hinweis, wer laut Excel eingetragen war. Auswahl wirkt sofort
(setSwap()), die Zeile klappt danach zu und ist mit „· getauscht” und
farbigem Tag markiert.dienstart_manuell gesetzt ist). swap liegt im JSON-Backup und
wird von „Alle Pläne löschen” mit geleert.resetTag hat nach delete days[cur] weder gespeichert
noch renderHistory() aufgerufen, (2) renderDay() legt den Tag sofort neu
an und trägt die Soll-Zeiten aus dem Dienstplan ein – der alte Verlaufsfilter
(d.dienstbeginn || …) hat das als „erfasst” gewertet.hasContent(k) entscheidet, ob ein Tag im Verlauf
erscheint: Ist-Dienstende, Einsätze, FRN, dienstart_manuell, ausgefüllte
Text-/Auswahlfelder ODER Dienstbeginn/Plan-Ende, die von den Soll-Zeiten der
abgeleiteten Dienstart abweichen. Bloßes Anschauen eines Tages legt damit
keinen Verlaufseintrag mehr an. resetTag speichert jetzt und rendert den
Verlauf neu.figma-design-to-code) wurde besprochen, aber vom Nutzer noch nicht
aktiviert/verbunden – falls gewünscht: claude.ai → Connector-Einstellungen.claude/peaceful-cerf-3v8uke wird ohne Rückfrage per Fast-Forward-Merge
nach main gepusht (kein PR-Workflow für dieses Repo).curl -d "{...}" kann
bei großen Dateien (hier: app/index.html mit eingebettetem Base64-PDF,
100 KB) die Fehlermeldung
curl: Argument list too longauftreten. Die Shell bricht dabei NICHT den ganzen Loop ab, sondern nur den einzelnen Befehl – das kann dazu führen, dass eine Datei im Loop übersprungen wird, obwohl der Log scheinbar “OK” für eine andere Datei zeigt. Fix: Bei Dateien >~50 KB immer den Payload zuerst perpython3in eine JSON-Datei schreiben (base64.b64encode+json.dump) und danncurl --data @payload.jsonverwenden, NIE-d "$(base64 ...)"inline. Nach jedem Push den Commit-Diff prüfen (GET /commits/{sha}→files[]), um zu verifizieren, welche Datei wirklich verändert wurde – nicht nur auf die “OK”-Ausgabe verlassen.
saveFormToCookie()/loadFormFromCookie() in BEIDEN Dateien
(Selektor .block müsste auf einen engeren Container eingeschränkt
werden, der nur die Kopfdaten umfasst).recalcDuty()/calc()
haben bei negativer Differenz pauschal 24h addiert (Mitternacht-Wrap-Annahme
für den regulären Dienst), dadurch kam bei frühem Dienstende ein falscher
großer positiver Wert raus statt Minusstunden. Fix in BEIDEN Dateien
(Desktop tagesdokumentation_erfassung.html/build_html.py + PWA
app/index.html): kein +24-Wrap mehr bei tats_dienstende < plan_dienstende.
Hinweis: Falls der reguläre Dienst je über Mitternacht gehen sollte, würde
das jetzt fälschlich negativ berechnet – bisher kein bekannter Anwendungsfall.5af1987b,
PWA-Commit 03d5d531).