schema.go
| 1 | // Package befund beschreibt das Befundformular als Daten. Formular, Druckansicht, |
| 2 | // Vergleich und Import werden alle aus diesem einen Schema erzeugt. |
| 3 | // |
| 4 | // Die Feldschlüssel entsprechen der früheren Befundseite (data-bind), damit alte |
| 5 | // JSON-Exporte 1:1 importiert werden können. |
| 6 | package befund |
| 7 | |
| 8 | type Field struct { |
| 9 | Key, Label, Placeholder string |
| 10 | Type string // "text" (Standard), "area", "select", "check", "nrs" (0–10) |
| 11 | Rows int |
| 12 | Options []string |
| 13 | Wide bool // volle Breite im Raster |
| 14 | } |
| 15 | |
| 16 | type Col struct { |
| 17 | Key, Label, Placeholder string |
| 18 | Type string // "text", "number", "date", "select" |
| 19 | Options []string |
| 20 | List string // id einer <datalist> mit Vorschlägen |
| 21 | } |
| 22 | |
| 23 | type Table struct { |
| 24 | Key string |
| 25 | Cols []Col |
| 26 | Match []string // Spalten, über die Zeilen zweier Befunde zugeordnet werden |
| 27 | Diff []string // Spalten, deren Werte verglichen werden |
| 28 | } |
| 29 | |
| 30 | // Part ist ein Abschnitt innerhalb einer Sektion: Feldraster, Tabelle oder Körperschema. |
| 31 | type Part struct { |
| 32 | Title string |
| 33 | Collapsible bool |
| 34 | Fields []Field |
| 35 | Table *Table |
| 36 | Body bool |
| 37 | } |
| 38 | |
| 39 | type Section struct { |
| 40 | ID, Title string |
| 41 | Specialty bool // Fachbereich: eingeklappt, außer er enthält Daten |
| 42 | Parts []Part |
| 43 | } |
| 44 | |
| 45 | func area(key, label string) Field { return Field{Key: key, Label: label, Type: "area"} } |
| 46 | func text(key, label string) Field { return Field{Key: key, Label: label} } |
| 47 | func wide(f Field) Field { f.Wide = true; return f } |
| 48 | func ph(f Field, p string) Field { f.Placeholder = p; return f } |
| 49 | func rows(f Field, n int) Field { f.Rows = n; return f } |
| 50 | func check(key, label string) Field { return Field{Key: key, Label: label, Type: "check"} } |
| 51 | func nrs(key, label string) Field { return Field{Key: key, Label: label, Type: "nrs"} } |
| 52 | |
| 53 | var ( |
| 54 | TableTimeline = &Table{Key: "timeline", Cols: []Col{ |
| 55 | {Key: "date", Label: "Datum", Type: "date"}, |
| 56 | {Key: "title", Label: "Ereignis"}, |
| 57 | {Key: "note", Label: "Kurzbeschreibung"}, |
| 58 | }} |
| 59 | TableROM = &Table{Key: "rom", Match: []string{"joint", "move"}, Diff: []string{"act", "pass", "end"}, Cols: []Col{ |
| 60 | {Key: "joint", Label: "Gelenk", Placeholder: "z. B. Knie re"}, |
| 61 | {Key: "move", Label: "Bewegung", Placeholder: "Flex/Ext"}, |
| 62 | {Key: "act", Label: "Aktiv", Placeholder: "0-0-130"}, |
| 63 | {Key: "pass", Label: "Passiv"}, |
| 64 | {Key: "end", Label: "Endgefühl", Placeholder: "z. B. elastisch"}, |
| 65 | }} |
| 66 | TableMFP = &Table{Key: "mfp", Match: []string{"muscle"}, Diff: []string{"right", "left"}, Cols: []Col{ |
| 67 | {Key: "muscle", Label: "Muskel / Gruppe", Placeholder: "z. B. Quadrizeps"}, |
| 68 | {Key: "right", Label: "rechts (0–5)", Placeholder: "z. B. 4+"}, |
| 69 | {Key: "left", Label: "links (0–5)", Placeholder: "z. B. 4"}, |
| 70 | }} |
| 71 | TableCirc = &Table{Key: "circ", Match: []string{"point"}, Diff: []string{"right", "left"}, Cols: []Col{ |
| 72 | {Key: "point", Label: "Messpunkt", Placeholder: "z. B. 10 cm unter Patella"}, |
| 73 | {Key: "right", Label: "rechts (cm)"}, |
| 74 | {Key: "left", Label: "links (cm)"}, |
| 75 | }} |
| 76 | TableTests = &Table{Key: "tests", Match: []string{"test", "side"}, Diff: []string{"result"}, Cols: []Col{ |
| 77 | {Key: "test", Label: "Test", Placeholder: "z. B. Lachman", List: "dl-tests"}, |
| 78 | {Key: "side", Label: "Seite", Type: "select", Options: []string{"", "re", "li", "bds", "–"}}, |
| 79 | {Key: "result", Label: "Ergebnis", Type: "select", Options: []string{"", "positiv", "negativ", "fraglich"}}, |
| 80 | {Key: "note", Label: "Bemerkung", Placeholder: "z. B. Schmerz reproduziert, Endgefühl"}, |
| 81 | }} |
| 82 | TableMeasures = &Table{Key: "measures", Match: []string{"tool"}, Diff: []string{"value"}, Cols: []Col{ |
| 83 | {Key: "tool", Label: "Instrument", Placeholder: "z. B. ODI, TUG", List: "dl-measures"}, |
| 84 | {Key: "value", Label: "Wert", Placeholder: "z. B. 34"}, |
| 85 | {Key: "unit", Label: "Einheit / Max.", Placeholder: "z. B. % · s · /80"}, |
| 86 | {Key: "note", Label: "Bemerkung"}, |
| 87 | }} |
| 88 | TablePSFS = &Table{Key: "psfs", Match: []string{"activity"}, Diff: []string{"score"}, Cols: []Col{ |
| 89 | {Key: "activity", Label: "Aktivität (vom Patienten genannt)", Placeholder: "z. B. Treppe steigen 2 Stockwerke"}, |
| 90 | {Key: "score", Label: "0–10", Type: "number"}, |
| 91 | }} |
| 92 | Tables = []*Table{TableTimeline, TableROM, TableMFP, TableCirc, TableTests, TableMeasures, TablePSFS} |
| 93 | |
| 94 | // Vorschläge für Instrumente (nur Namen; Werte trägt man selbst ein) |
| 95 | Measures = []string{ |
| 96 | "NRS Schmerz", "PSFS (Mittelwert)", "ODI", "RMDQ", "NDI", "KOOS", "HOOS", "LEFS", "QuickDASH", "SPADI", |
| 97 | "VISA-A", "VISA-P", "FAAM", "TSK", "PCS", "STarT Back", "Örebro (Kurzform)", "EQ-5D", |
| 98 | "TUG", "30s Chair Stand", "5x Sit-to-Stand", "6MWT", "10m Gehtest", "BBS", "Tinetti", "Einbeinstand", |
| 99 | "Handkraft (Dynamometer)", "Y-Balance", "Single Leg Hop", "Borg (RPE)", |
| 100 | } |
| 101 | ) |
| 102 | |
| 103 | var Sections = []Section{ |
| 104 | {ID: "patient", Title: "Patientendaten", Parts: []Part{{Fields: []Field{ |
| 105 | text("diag", "Ärztl. Diagnose"), |
| 106 | text("onset", "Datum der Schädigung/Erkrankung/OP"), |
| 107 | area("coDiag", "Nebendiagnosen"), |
| 108 | area("meds", "Medikamente und Dosierung"), |
| 109 | area("contra", "Kontraindikationen"), |
| 110 | area("loadLimit", "Belastbarkeit (ärztl. Vorgaben/Grenze)"), |
| 111 | area("nursing", "Hinweise aus der Pflege"), |
| 112 | area("tools", "Hilfsmittel"), |
| 113 | wide(area("firstHyp", "Erste Hypothese/erwartbare Probleme")), |
| 114 | }}}}, |
| 115 | {ID: "screening", Title: "Screening: Red & Yellow Flags", Parts: []Part{ |
| 116 | {Title: "Red Flags (Hinweise auf ernste Pathologie)", Fields: []Field{ |
| 117 | check("rf_cancer", "Krebserkrankung in der Vorgeschichte"), |
| 118 | check("rf_weight", "Ungewollter Gewichtsverlust"), |
| 119 | check("rf_fever", "Fieber, Infektzeichen, Immunsuppression"), |
| 120 | check("rf_night", "Ruhe-/Nachtschmerz ohne Bewegungsbezug"), |
| 121 | check("rf_trauma", "Relevantes Trauma / Osteoporose, Kortison (Fraktur?)"), |
| 122 | check("rf_cauda", "Reithosenanästhesie, Blasen-/Mastdarmstörung"), |
| 123 | check("rf_neuro", "Progrediente neurologische Ausfälle"), |
| 124 | check("rf_vasc", "Hinweise auf TVT/LE, kardiovaskuläre Ursache"), |
| 125 | check("rf_unwell", "Allgemeines Krankheitsgefühl, systemische Symptome"), |
| 126 | wide(ph(area("rf_action", "Bewertung & Vorgehen"), "z. B. keine Red Flags · Arzt am … informiert · Behandlung nur nach Abklärung")), |
| 127 | }}, |
| 128 | {Title: "Yellow / Blue Flags (Chronifizierungsrisiko)", Fields: []Field{ |
| 129 | check("yf_fear", "Angst-Vermeidungsverhalten"), |
| 130 | check("yf_cata", "Katastrophisieren"), |
| 131 | check("yf_mood", "Niedergeschlagenheit, Angst, Stress"), |
| 132 | check("yf_expect", "Geringe Erwartung an Besserung, passive Bewältigung"), |
| 133 | check("yf_work", "Arbeitsplatzprobleme, Konflikte, fehlende Unterstützung (Blue Flags)"), |
| 134 | wide(ph(text("screen_tool", "Screening-Instrument & Ergebnis"), "z. B. STarT Back: mittleres Risiko · Örebro-Kurzform: 52")), |
| 135 | }}, |
| 136 | }}, |
| 137 | {ID: "anamnese", Title: "Anamnese", Parts: []Part{ |
| 138 | {Title: "Körperschema", Body: true}, |
| 139 | {Title: "Schmerz", Fields: []Field{ |
| 140 | nrs("pain_now", "NRS aktuell (0–10)"), |
| 141 | nrs("pain_best", "NRS beste 24 h"), |
| 142 | nrs("pain_worst", "NRS schlimmste 24 h"), |
| 143 | { |
| 144 | Key: "pain_type", Label: "Dominanter Schmerzmechanismus", Type: "select", |
| 145 | Options: []string{"", "nozizeptiv", "neuropathisch", "noziplastisch", "gemischt", "unklar"}, |
| 146 | }, |
| 147 | }}, |
| 148 | {Fields: []Field{ |
| 149 | wide(rows(ph(area("anaIntro", "Einleitende Frage(n)"), "Was führt Sie zu mir? …"), 5)), |
| 150 | }}, |
| 151 | {Title: "Zeitlicher Verlauf", Table: TableTimeline}, |
| 152 | {Fields: []Field{ |
| 153 | area("24h", "24h-Verlauf"), |
| 154 | area("provo", "Provokation"), |
| 155 | text("lind", "Linderung"), |
| 156 | text("S/I", "Schwere/Irritierbarkeit"), |
| 157 | area("nas", "NAS nach Situation, Qualität etc."), |
| 158 | area("anaLoad", "Alltägliche Belastung (Beruf, Sport, Hobby, Tätigkeit)"), |
| 159 | area("envPos", "Umweltfaktoren – positiv"), |
| 160 | area("envNeg", "Umweltfaktoren – negativ"), |
| 161 | }}, |
| 162 | {Title: "Allgemeiner Eindruck, Orientierung, Psyche, Kognition, Adhärenz", Collapsible: true, Fields: []Field{ |
| 163 | text("psyche", "Psyche"), |
| 164 | text("cognition", "Kognition"), |
| 165 | wide(area("adh", "Compliance/Adhärenz/Sonstiges")), |
| 166 | }}, |
| 167 | }}, |
| 168 | {ID: "inspektion", Title: "Inspektion", Parts: []Part{{Fields: []Field{ |
| 169 | wide(area("inspection", "Inspektionsbefund (Haltung, Haut, Ödeme, Narben, Atmung etc.)")), |
| 170 | }}}}, |
| 171 | {ID: "bps", Title: "Psychosoziale Anamnese", Parts: []Part{{Fields: []Field{ |
| 172 | area("bps_family", "Familie/soziales Netz"), |
| 173 | area("bps_home", "Wohn-/Lebenssituation"), |
| 174 | area("bps_work", "Beruf/Arbeitsbelastung"), |
| 175 | area("bps_leisure", "Freizeit/Aktivität"), |
| 176 | ph(area("bps_activity", "Körperliche Aktivität"), "Art, Min./Woche moderat/intensiv, Krafttraining?"), |
| 177 | area("bps_sleep", "Schlaf"), |
| 178 | wide(ph(area("bps_expect", "Vorstellungen & Erwartungen des Patienten"), "Eigene Erklärung der Beschwerden, erwarteter Verlauf, Wunsch an die Behandlung")), |
| 179 | }}}}, |
| 180 | {ID: "hyp-anam", Title: "Hypothese nach Anamnese", Parts: []Part{{Fields: []Field{ |
| 181 | wide(rows(area("hypAfterAnam", ""), 4)), |
| 182 | }}}}, |
| 183 | {ID: "exam", Title: "Körperliche Untersuchung – Basis", Parts: []Part{ |
| 184 | {Title: "ROM (Neutral-Null)", Table: TableROM}, |
| 185 | {Title: "MFP (Muskelkraftprüfung)", Fields: []Field{wide(ph(text("mfp-allgemein", "Allgemein"), "z. B. Seitenvergleich, Ermüdung"))}, Table: TableMFP}, |
| 186 | {Title: "Umfangmessung", Collapsible: true, Table: TableCirc}, |
| 187 | {Title: "Spezifische Tests", Table: TableTests}, |
| 188 | {Title: "Assessments & PROMs (Messwerte für den Verlauf)", Table: TableMeasures}, |
| 189 | {Title: "Ganganalyse", Collapsible: true, Fields: []Field{wide(ph(area("gait", ""), "Gangphase, Spurbreite, Armpendel, Ausweichmechanismen …"))}}, |
| 190 | {Title: "Assessments – Notizen", Collapsible: true, Fields: []Field{wide(ph(area("assess", ""), "Durchführung, Auffälligkeiten (Werte bitte in die Tabelle oben)"))}}, |
| 191 | {Title: "Sensibilität/Wahrnehmung", Collapsible: true, Fields: []Field{wide(ph(area("sens", ""), "Reflextests, Dermatome …"))}}, |
| 192 | {Title: "Neurologische Untersuchung", Collapsible: true, Fields: []Field{ |
| 193 | area("neuro_derm", "Sensibilität nach Dermatomen"), |
| 194 | area("neuro_myo", "Kennmuskeln/Myotome"), |
| 195 | area("neuro_reflex", "Reflexe (MER, Babinski)"), |
| 196 | area("neuro_dyn", "Neurodynamik (SLR, Slump, ULNT, PKB)"), |
| 197 | }}, |
| 198 | {Title: "Sicherheitstests", Collapsible: true, Fields: []Field{wide(ph(area("safetyNeuro", ""), "z. B. Instabilitätstests HWS, vaskuläre Tests"))}}, |
| 199 | }}, |
| 200 | |
| 201 | // ---- Fachbereiche ---- |
| 202 | {ID: "ortho", Title: "Chirurgie/Orthopädie", Specialty: true, Parts: []Part{{Fields: []Field{ |
| 203 | area("or_jointProv", "Gelenktest/Provokation"), |
| 204 | area("or_len", "Muskellänge – Details"), |
| 205 | }}}}, |
| 206 | {ID: "gyn-geb", Title: "Gynäkologie – Geburtshilfe", Specialty: true, Parts: []Part{ |
| 207 | {Fields: []Field{ |
| 208 | text("gg_gravida", "Schwangerschaften (Gravida)"), |
| 209 | text("gg_para", "Geburten (Para)"), |
| 210 | area("gg_pregCourse", "Schwangerschaftsverlauf"), |
| 211 | area("gg_pregFindings", "Auffälligkeiten in der Schwangerschaft"), |
| 212 | text("gg_mode", "Geburtsmodus"), |
| 213 | area("gg_birthCourse", "Geburtsverlauf"), |
| 214 | text("gg_position", "Gebärposition"), |
| 215 | area("gg_comp", "Komplikationen"), |
| 216 | }}, |
| 217 | {Title: "Angaben zum Kind", Collapsible: true, Fields: []Field{ |
| 218 | text("gg_apgar", "APGAR"), |
| 219 | text("gg_child", "Name/Geschlecht"), |
| 220 | text("gg_stats", "Größe/Gewicht/KU"), |
| 221 | }}, |
| 222 | {Fields: []Field{ |
| 223 | area("gg_pelvic", "Beckenbodenbefund"), |
| 224 | area("gg_abdomen", "Bauchbefund"), |
| 225 | }}, |
| 226 | }}, |
| 227 | {ID: "gyn-uro", Title: "Gynäkologie/Urologie", Specialty: true, Parts: []Part{{Fields: []Field{ |
| 228 | area("gu_pelvic", "Beckenbodenbefund"), |
| 229 | area("gu_abdomen", "Bauchbefund"), |
| 230 | }}}}, |
| 231 | {ID: "innere", Title: "Innere Medizin", Specialty: true, Parts: []Part{ |
| 232 | {Fields: []Field{ |
| 233 | text("im_pulse_point", "Puls (Messpunkt)"), |
| 234 | ph(text("im_pulse_freqs", "Frequenz Ruhe/Belastung/nachher"), "z. B. 68 / 102 / 80"), |
| 235 | {Key: "im_pulse_rhythm", Label: "Pulsrhythmus", Type: "select", Options: []string{"", "regelmäßig", "unregelmäßig"}}, |
| 236 | text("im_pulse_fill", "Pulsfüllung"), |
| 237 | ph(text("im_rr", "RR (Ruhe/Belastung/nachher)"), "z. B. 120/70 / …"), |
| 238 | text("im_meds", "Beeinflussende Medikamente"), |
| 239 | wide(area("im_breath", "Atemdiagramm (CSV/CSL/CAL/CAV | ARL/MI/ME)")), |
| 240 | }}, |
| 241 | {Title: "Thrombosedruckpunkte (positiv)", Fields: []Field{ |
| 242 | check("im_payr", "Payr"), check("im_homann", "Homans"), check("im_adduct", "Adduktoren"), |
| 243 | check("im_meyer", "Meyer"), check("im_bisgard", "Bisgaard"), |
| 244 | }}, |
| 245 | }}, |
| 246 | {ID: "neuro", Title: "Neurologie", Specialty: true, Parts: []Part{{Fields: []Field{ |
| 247 | wide(area("ne_tone", "Muskeltonus (Spastik, Klonus, Rigor, Tremor, Akinese, FoG, Ataxie)")), |
| 248 | area("ne_sens", "Sensibilität (Seitenvergleich)"), |
| 249 | area("ne_prop", "Propriozeption (UEX/OEX)"), |
| 250 | area("ne_balance", "Romberg/Einbeinstand"), |
| 251 | area("ne_reflex", "Holding/Placing/Mirroring"), |
| 252 | }}}}, |
| 253 | {ID: "paedo", Title: "Pädiatrie", Specialty: true, Parts: []Part{{Fields: []Field{ |
| 254 | area("pa_breath", "Atmung (Sekret, Husten, Atemgeräusche, Atelektasen …)"), |
| 255 | area("pa_vitals", "Vitalfunktionen (Herzfrequenz, Puls, RR)"), |
| 256 | area("pa_tone", "Muskeltonus"), |
| 257 | area("pa_fine", "Feinmotorik"), |
| 258 | text("pa_gmfcs", "GMFCS"), |
| 259 | text("pa_mac", "MACS"), |
| 260 | area("pa_adl", "ADL/Selbstständigkeit"), |
| 261 | area("pa_move", "Bewegungsanalyse"), |
| 262 | area("pa_play", "Situations-/Spielverhalten"), |
| 263 | area("pa_assess", "Assessments"), |
| 264 | }}}}, |
| 265 | |
| 266 | // ---- Auswertung ---- |
| 267 | {ID: "hyp-exam", Title: "Hypothese nach körperlicher Untersuchung", Parts: []Part{{Fields: []Field{ |
| 268 | wide(rows(ph(area("hypAfterExam", "Arbeitshypothese"), "Was spricht dafür (Anamnese, Tests, Messwerte)?"), 4)), |
| 269 | wide(ph(area("ddx", "Differenzialdiagnosen"), "Alternative Erklärungen – und was dagegen spricht")), |
| 270 | }}}}, |
| 271 | {ID: "icf", Title: "ICF – Hauptproblem und Problemanalyse", Parts: []Part{{Fields: []Field{ |
| 272 | area("icf_part", "Partizipationsebene"), |
| 273 | area("icf_activity", "Aktivitätsebene"), |
| 274 | wide(area("icf_body", "Funktions- und Strukturebene")), |
| 275 | area("goal_patient", "Patienten-/Angehörigenziel"), |
| 276 | area("goal_pt", "PT-Ziel (SMART)"), |
| 277 | wide(area("goal_agreement", "Zielvereinbarung")), |
| 278 | }}, {Title: "PSFS – Patientenspezifische Funktionsskala (0 = nicht möglich, 10 = wie vor dem Problem)", Table: TablePSFS}}}, |
| 279 | {ID: "prognosis", Title: "Prognose mit Begründung", Parts: []Part{{Fields: []Field{ |
| 280 | wide(rows(area("prognosis", ""), 4)), |
| 281 | }}}}, |
| 282 | {ID: "plan", Title: "Therapieplan", Parts: []Part{{Fields: []Field{ |
| 283 | wide(rows(area("plan", "Plan (Methoden, Frequenz, Edukation, Heimprogramm, Hilfsmittel)"), 5)), |
| 284 | wide(ph(area("plan_evidence", "Begründung / Evidenz"), "Leitlinie, Studienlage, Protokoll aus der Wissensdatenbank")), |
| 285 | ph(text("plan_reassess", "Re-Assessment geplant"), "z. B. nach 6 Behandlungen: NRS, PSFS, ODI"), |
| 286 | check("consent", "Aufklärung über Befund, Prognose und Plan erfolgt; Einwilligung liegt vor"), |
| 287 | }}}}, |
| 288 | {ID: "zusatz", Title: "Zusätzliche Notizen", Parts: []Part{{Fields: []Field{ |
| 289 | wide(rows(area("zusatz", ""), 4)), |
| 290 | }}}}, |
| 291 | } |
| 292 | |
| 293 | // Symbole für das Körperschema (wie auf der alten Befundseite). |
| 294 | type Symbol struct{ Key, Char, Name string } |
| 295 | |
| 296 | var Symbols = []Symbol{ |
| 297 | {"pain", "⚡\ufe0e", "Schmerz"}, |
| 298 | {"swelling", "(( ))", "Schwellung"}, |
| 299 | {"fracture", "◆", "Fraktur"}, |
| 300 | {"tender", "×", "Druckschmerz"}, |
| 301 | {"lesion", "●", "Läsion"}, |
| 302 | {"radiating", "→", "Ausstrahlung"}, |
| 303 | {"sensory", "≈", "Sensibilitätsstörung"}, |
| 304 | } |
| 305 | |
| 306 | // Ansichten des Körperschemas; W/H = Bildgröße, dient als SVG-viewBox. |
| 307 | type View struct { |
| 308 | Name, Image string |
| 309 | W, H int |
| 310 | } |
| 311 | |
| 312 | var Views = []View{ |
| 313 | {"Vorne", "/static/body/body-front.png", 78, 180}, |
| 314 | {"Hinten", "/static/body/body-back.png", 84, 176}, |
| 315 | {"Seitlich", "/static/body/body-side.png", 106, 181}, |
| 316 | } |
| 317 | |
| 318 | var fieldIndex = func() map[string]Field { |
| 319 | m := map[string]Field{} |
| 320 | for _, s := range Sections { |
| 321 | for _, p := range s.Parts { |
| 322 | for _, f := range p.Fields { |
| 323 | if _, dup := m[f.Key]; dup { |
| 324 | panic("doppelter Feldschlüssel: " + f.Key) |
| 325 | } |
| 326 | m[f.Key] = f |
| 327 | } |
| 328 | } |
| 329 | } |
| 330 | return m |
| 331 | }() |
| 332 | |
| 333 | func FieldByKey(k string) (Field, bool) { f, ok := fieldIndex[k]; return f, ok } |
| 334 |