Initial commit
This commit is contained in:
commit
3cba772836
1341 changed files with 532924 additions and 0 deletions
BIN
SOMS2/.RData
Normal file
BIN
SOMS2/.RData
Normal file
Binary file not shown.
1
SOMS2/.Rprofile
Normal file
1
SOMS2/.Rprofile
Normal file
|
|
@ -0,0 +1 @@
|
|||
source("renv/activate.R")
|
||||
13
SOMS2/SOMS2.Rproj
Normal file
13
SOMS2/SOMS2.Rproj
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
Version: 1.0
|
||||
|
||||
RestoreWorkspace: Default
|
||||
SaveWorkspace: Default
|
||||
AlwaysSaveHistory: Default
|
||||
|
||||
EnableCodeIndexing: Yes
|
||||
UseSpacesForTab: Yes
|
||||
NumSpacesForTab: 2
|
||||
Encoding: UTF-8
|
||||
|
||||
RnwWeave: Sweave
|
||||
LaTeX: pdfLaTeX
|
||||
999
SOMS2/app.R
Normal file
999
SOMS2/app.R
Normal file
|
|
@ -0,0 +1,999 @@
|
|||
# Präambel ####
|
||||
|
||||
AKZENT_FARBE = "#8B2635"
|
||||
|
||||
PFAD_DOWNLOAD_SKRIPT = "../API/get_data_soms2.R" # liefert: daten_soms2
|
||||
PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R" # liefert: pseudo
|
||||
|
||||
PFAD_NORM_A1 = "normen/soms2_norm_a1_gesunde.csv"
|
||||
PFAD_NORM_A2 = "normen/soms2_norm_a2_patienten.csv"
|
||||
|
||||
SOMS2_ROHWERT_MAX_A1 = 20
|
||||
SOMS2_ROHWERT_MAX_A2 = 40
|
||||
|
||||
SOMS2_DISCLAIMER = paste0(
|
||||
"Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ",
|
||||
"keine klinische Diagnose. Die Interpretation obliegt der behandelnden Person."
|
||||
)
|
||||
|
||||
SOMS2_BESCHWERDEN_CUTOFF_HINWEIS = paste0(
|
||||
"Ab einem Beschwerdenindex von mindestens 7 im SOMS-2 kann in der Regel von ",
|
||||
"einem starken, beeintraechtigenden Somatisierungssyndrom ausgegangen werden."
|
||||
)
|
||||
|
||||
library(shiny)
|
||||
library(dplyr)
|
||||
library(haven)
|
||||
library(officer)
|
||||
|
||||
|
||||
# Infrastruktur ####
|
||||
|
||||
APP_VERZEICHNIS = normalizePath(getwd())
|
||||
|
||||
absPath = function(pfad) {
|
||||
if (grepl("^([A-Za-z]:[/\\\\]|/)", pfad)) return(pfad)
|
||||
file.path(APP_VERZEICHNIS, pfad)
|
||||
}
|
||||
|
||||
PFAD_DOWNLOAD_SKRIPT = normalizePath(absPath(PFAD_DOWNLOAD_SKRIPT), mustWork = FALSE)
|
||||
PFAD_PSEUDONYM_SKRIPT = normalizePath(absPath(PFAD_PSEUDONYM_SKRIPT), mustWork = FALSE)
|
||||
PFAD_NORM_A1 = normalizePath(absPath(PFAD_NORM_A1), mustWork = FALSE)
|
||||
PFAD_NORM_A2 = normalizePath(absPath(PFAD_NORM_A2), mustWork = FALSE)
|
||||
|
||||
|
||||
# Helper ####
|
||||
|
||||
# Generischer Label-Text-Lookup ueber das labels-Attribut der ORIGINAL-Spalte
|
||||
# (vor Subsetting), damit die Zuordnung Wert -> Text immer aus den Daten selbst
|
||||
# stammt und nie von der konkreten formr-Rohkodierung abhaengt.
|
||||
soms2_get_label_text = function(original_col, wert) {
|
||||
if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(NA_character_)
|
||||
lbl_attr = attr(original_col, "labels")
|
||||
if (!is.null(lbl_attr) && length(lbl_attr) > 0) {
|
||||
pos = which(as.vector(lbl_attr) == as.numeric(wert[1]))
|
||||
if (length(pos) > 0) return(trimws(names(lbl_attr)[pos[1]]))
|
||||
}
|
||||
NA_character_
|
||||
}
|
||||
|
||||
# Ordinalrang (0-basiert) ueber die sortierte Position im labels-Attribut,
|
||||
# unabhaengig von der tatsaechlichen formr-Kodierung. Genutzt fuer soms2_54
|
||||
# (0 = "keinmal" ... 4 = "mehr als 12 mal") und soms2_63
|
||||
# (0 = "unter 6 Monate" ... 3 = "ueber 2 Jahre").
|
||||
soms2_ordinal_rang = function(original_col, wert) {
|
||||
if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(NA_integer_)
|
||||
lbl_attr = attr(original_col, "labels")
|
||||
if (!is.null(lbl_attr) && length(lbl_attr) > 0) {
|
||||
lbl_sortiert = sort(as.vector(lbl_attr))
|
||||
pos = which(lbl_sortiert == as.numeric(wert[1]))
|
||||
if (length(pos) > 0) return(as.integer(pos[1]) - 1L)
|
||||
}
|
||||
NA_integer_
|
||||
}
|
||||
|
||||
# TRUE nur bei eindeutigem "ja"-Label. NA/fehlend (u.a. showif-bedingtes NA bei
|
||||
# geschlechtsspezifischen Items) wird als "nein" gewertet, nie als Fehler - das
|
||||
# ist fuer die Summenscores korrekt, weil solche Items ohnehin ueber den
|
||||
# dynamischen Maximalwert aus der Zaehlung herausgenommen werden.
|
||||
soms2_ist_ja = function(original_col, wert) {
|
||||
txt = soms2_get_label_text(original_col, wert)
|
||||
if (is.na(txt)) return(FALSE)
|
||||
if (grepl("nein", txt, ignore.case = TRUE)) return(FALSE)
|
||||
grepl("ja", txt, ignore.case = TRUE)
|
||||
}
|
||||
|
||||
# TRUE nur bei explizitem "nein"-Label. Anders als soms2_ist_ja() liefert dies
|
||||
# bei NA FALSE zurueck (Kriterium "Antwort = nein" gilt bei fehlender Antwort
|
||||
# als nicht erfuellt, nicht automatisch als erfuellt).
|
||||
soms2_ist_nein = function(original_col, wert) {
|
||||
txt = soms2_get_label_text(original_col, wert)
|
||||
if (is.na(txt)) return(FALSE)
|
||||
grepl("nein", txt, ignore.case = TRUE)
|
||||
}
|
||||
|
||||
# Geschlecht ausschliesslich ueber das labels-Attribut/den Text abgleichen, nie
|
||||
# ueber hartkodierte Zahlenwerte 1/2 (Kodierung kann je Setup variieren).
|
||||
soms2_geschlecht_text = function(original_col, wert) {
|
||||
txt = soms2_get_label_text(original_col, wert)
|
||||
if (is.na(txt)) return(NA_character_)
|
||||
txt_l = tolower(txt)
|
||||
if (grepl("weib", txt_l)) return("weiblich")
|
||||
if (grepl("männ|maenn", txt_l)) return("maennlich")
|
||||
NA_character_
|
||||
}
|
||||
|
||||
# Entfernt Markdown-Escapes ("1\. Text" -> "1. Text", so liegt das label-
|
||||
# Attribut in der soms2.xlsx vor) und danach die fuehrende Itemnummer samt
|
||||
# Trennzeichen (z.B. "1. " oder "01) ").
|
||||
soms2_clean_item_text = function(text) {
|
||||
if (is.null(text) || length(text) == 0 || is.na(text[1])) return(NA_character_)
|
||||
txt = gsub("\\.", ".", trimws(as.character(text[1])), fixed = TRUE)
|
||||
trimws(sub("^\\d+[.):]?\\s*", "", txt))
|
||||
}
|
||||
|
||||
# Klartext-Fragetext eines Items, Fallback auf den Variablennamen, falls kein
|
||||
# label-Attribut vorhanden ist (z.B. bei einem unvollstaendigen Test-Stub).
|
||||
soms2_item_text = function(daten, item) {
|
||||
txt = soms2_clean_item_text(attr(daten[[item]], "label"))
|
||||
if (is.na(txt)) item else txt
|
||||
}
|
||||
|
||||
# Baut einen Kriteriumsbestandteil: Fragetext, tatsaechlich gegebene Antwort
|
||||
# (Klartext ueber das labels-Attribut) und geforderte Antwort/Kategorie.
|
||||
soms2_kriterium_teil = function(zeile, daten, item, erforderlich) {
|
||||
antwort = soms2_get_label_text(daten[[item]], zeile[[item]])
|
||||
list(
|
||||
text = soms2_item_text(daten, item),
|
||||
antwort = if (is.na(antwort)) "k. A." else antwort,
|
||||
erforderlich = erforderlich
|
||||
)
|
||||
}
|
||||
|
||||
# Zaehlt "ja"-Antworten ueber eine einfache Itemliste (DSM-IV, Beschwerdenindex).
|
||||
soms2_score_items = function(zeile, daten, items) {
|
||||
sum(vapply(items, function(it) soms2_ist_ja(daten[[it]], zeile[[it]]), logical(1)))
|
||||
}
|
||||
|
||||
# Zaehlt Wertungseinheiten: jede Gruppe (ODER-Verknuepfung mehrerer Items)
|
||||
# zaehlt maximal 1 Punkt, auch wenn mehrere Items darin "ja" sind
|
||||
# (ICD-10-Somatisierungsindex, SAD-Index).
|
||||
soms2_score_einheiten = function(zeile, daten, einheiten) {
|
||||
sum(vapply(einheiten, function(gruppe) {
|
||||
any(vapply(gruppe, function(it) soms2_ist_ja(daten[[it]], zeile[[it]]), logical(1)))
|
||||
}, logical(1)))
|
||||
}
|
||||
|
||||
# Prozentrang-Lookup: exakter Rohwert-Match; Rohwerte oberhalb des
|
||||
# Tabellenmaximums erhalten Prozentrang 100 (alle Indizes erreichen dort
|
||||
# bereits 100 in der jeweiligen Norm). Ohne exakten Treffer (sollte bei
|
||||
# Ganzzahl-Scores nicht vorkommen) wird auf den naechstniedrigeren
|
||||
# Tabelleneintrag geruendet.
|
||||
soms2_prozentrang = function(rohwert, norm_tab, spalte, rohwert_max) {
|
||||
if (is.null(rohwert) || length(rohwert) == 0 || is.na(rohwert)) return(NA_integer_)
|
||||
if (rohwert > rohwert_max) return(100L)
|
||||
treffer = norm_tab[norm_tab$rohwert == rohwert, ]
|
||||
if (nrow(treffer) == 0) {
|
||||
kandidaten = norm_tab[norm_tab$rohwert <= rohwert, ]
|
||||
if (nrow(kandidaten) == 0) return(NA_integer_)
|
||||
treffer = kandidaten[which.max(kandidaten$rohwert), ]
|
||||
}
|
||||
as.numeric(treffer[[spalte]][1])
|
||||
}
|
||||
|
||||
# Ermittelt Prozentrang (+ optionale Gesamtnorm-Referenz bei A-1) fuer einen
|
||||
# Index, abhaengig von der gewaehlten Vergleichsnorm und dem Geschlecht.
|
||||
soms2_pr_ergebnis = function(index_key, rohwert, geschlecht, normwahl, norm_a1, norm_a2) {
|
||||
if (identical(normwahl, "patienten")) {
|
||||
spalte = SOMS2_NORM_SPALTEN_A2[[index_key]]
|
||||
pr = soms2_prozentrang(rohwert, norm_a2, spalte, SOMS2_ROHWERT_MAX_A2)
|
||||
return(list(pr = pr, pr_gesamt = NA_integer_))
|
||||
}
|
||||
spalten = SOMS2_NORM_SPALTEN_A1[[index_key]]
|
||||
geschlecht_spalte = if (identical(geschlecht, "weiblich")) spalten$w
|
||||
else if (identical(geschlecht, "maennlich")) spalten$m
|
||||
else spalten$gesamt
|
||||
pr = soms2_prozentrang(rohwert, norm_a1, geschlecht_spalte, SOMS2_ROHWERT_MAX_A1)
|
||||
pr_gesamt = soms2_prozentrang(rohwert, norm_a1, spalten$gesamt, SOMS2_ROHWERT_MAX_A1)
|
||||
list(pr = pr, pr_gesamt = pr_gesamt)
|
||||
}
|
||||
|
||||
# Dynamische Maximalwerte (Abschnitt 4): unbekanntes/fehlendes Geschlecht faellt
|
||||
# konservativ auf den vollen (nicht reduzierten) Maximalwert zurueck.
|
||||
soms2_dsmiv_max = function(geschlecht) {
|
||||
if (identical(geschlecht, "weiblich")) return(33L - 1L)
|
||||
if (identical(geschlecht, "maennlich")) return(33L - 4L)
|
||||
33L
|
||||
}
|
||||
soms2_icd10_max = function(geschlecht) {
|
||||
if (identical(geschlecht, "maennlich")) return(13L)
|
||||
14L
|
||||
}
|
||||
soms2_beschwerden_max = function(geschlecht) {
|
||||
# Beschwerdenindex umfasst alle 53 Items (anders als DSM-IV, das soms2_52
|
||||
# gar nicht enthaelt): bei Maennern sind soms2_48-52 (5 Items) nicht
|
||||
# anwendbar, bei Frauen nur soms2_53 (1 Item).
|
||||
if (identical(geschlecht, "weiblich")) return(53L - 1L)
|
||||
if (identical(geschlecht, "maennlich")) return(53L - 5L)
|
||||
53L
|
||||
}
|
||||
|
||||
# Jedes Kriterium traegt seine Bestandteile (teile - i.d.R. 1, bei der
|
||||
# ODER-Verknuepfung in Kriterium 1 des DSM-IV-Index 2) mit dem tatsaechlichen
|
||||
# Fragetext + der gegebenen Antwort, damit UI und Word-Export die konkrete
|
||||
# Item-Formulierung statt einer blossen Itemnummer anzeigen koennen.
|
||||
soms2_dsmiv_kriterien = function(zeile, daten) {
|
||||
rang54 = soms2_ordinal_rang(daten[["soms2_54"]], zeile[["soms2_54"]])
|
||||
rang63 = soms2_ordinal_rang(daten[["soms2_63"]], zeile[["soms2_63"]])
|
||||
list(
|
||||
list(
|
||||
teile = list(
|
||||
soms2_kriterium_teil(zeile, daten, "soms2_54", "nicht 'keinmal'"),
|
||||
soms2_kriterium_teil(zeile, daten, "soms2_58", "ja")
|
||||
),
|
||||
verknuepfung = "ODER",
|
||||
erfuellt = (!is.na(rang54) && rang54 != 0L) ||
|
||||
soms2_ist_ja(daten[["soms2_58"]], zeile[["soms2_58"]])
|
||||
),
|
||||
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_55", "nein")),
|
||||
verknuepfung = NULL,
|
||||
erfuellt = soms2_ist_nein(daten[["soms2_55"]], zeile[["soms2_55"]])),
|
||||
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_62", "ja")),
|
||||
verknuepfung = NULL,
|
||||
erfuellt = soms2_ist_ja(daten[["soms2_62"]], zeile[["soms2_62"]])),
|
||||
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_63", "'ueber 2 Jahre'")),
|
||||
verknuepfung = NULL,
|
||||
erfuellt = !is.na(rang63) && rang63 == 3L)
|
||||
)
|
||||
}
|
||||
|
||||
soms2_icd10_kriterien = function(zeile, daten) {
|
||||
rang54 = soms2_ordinal_rang(daten[["soms2_54"]], zeile[["soms2_54"]])
|
||||
rang63 = soms2_ordinal_rang(daten[["soms2_63"]], zeile[["soms2_63"]])
|
||||
list(
|
||||
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_54", "mindestens '3 bis 6 mal'")),
|
||||
verknuepfung = NULL, erfuellt = !is.na(rang54) && rang54 >= 2L),
|
||||
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_55", "nein")),
|
||||
verknuepfung = NULL, erfuellt = soms2_ist_nein(daten[["soms2_55"]], zeile[["soms2_55"]])),
|
||||
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_56", "nein")),
|
||||
verknuepfung = NULL, erfuellt = soms2_ist_nein(daten[["soms2_56"]], zeile[["soms2_56"]])),
|
||||
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_57", "ja")),
|
||||
verknuepfung = NULL, erfuellt = soms2_ist_ja(daten[["soms2_57"]], zeile[["soms2_57"]])),
|
||||
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_61", "nein")),
|
||||
verknuepfung = NULL, erfuellt = soms2_ist_nein(daten[["soms2_61"]], zeile[["soms2_61"]])),
|
||||
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_63", "'ueber 2 Jahre'")),
|
||||
verknuepfung = NULL, erfuellt = !is.na(rang63) && rang63 == 3L)
|
||||
)
|
||||
}
|
||||
|
||||
soms2_sad_kriterien = function(zeile, daten) {
|
||||
list(
|
||||
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_55", "nein")),
|
||||
verknuepfung = NULL, erfuellt = soms2_ist_nein(daten[["soms2_55"]], zeile[["soms2_55"]])),
|
||||
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_61", "nein")),
|
||||
verknuepfung = NULL, erfuellt = soms2_ist_nein(daten[["soms2_61"]], zeile[["soms2_61"]]))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# Datenaufbereitung ####
|
||||
|
||||
if (!file.exists(PFAD_NORM_A1)) {
|
||||
stop(paste0(
|
||||
"Normtabelle (Gesunde) nicht gefunden: ", PFAD_NORM_A1,
|
||||
". Bitte die Datei soms2_norm_a1_gesunde.csv in den Unterordner 'normen/' legen."
|
||||
))
|
||||
}
|
||||
if (!file.exists(PFAD_NORM_A2)) {
|
||||
stop(paste0(
|
||||
"Normtabelle (Patienten) nicht gefunden: ", PFAD_NORM_A2,
|
||||
". Bitte die Datei soms2_norm_a2_patienten.csv in den Unterordner 'normen/' legen."
|
||||
))
|
||||
}
|
||||
|
||||
soms2_norm_a1 = read.csv(PFAD_NORM_A1, stringsAsFactors = FALSE)
|
||||
soms2_norm_a2 = read.csv(PFAD_NORM_A2, stringsAsFactors = FALSE)
|
||||
|
||||
# Somatisierungsindex DSM-IV: 33 Items, soms2_53 nur bei Frauen erhoben,
|
||||
# soms2_48-51 nur bei Maennern erhoben (showif in formr).
|
||||
SOMS2_DSMIV_ITEMS = sprintf("soms2_%02d", c(
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 16, 20, 32, 34, 35,
|
||||
36, 37, 38, 39, 40, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 53
|
||||
))
|
||||
|
||||
# Somatisierungsindex ICD-10: 14 Wertungseinheiten (ODER-Gruppen), Einheit 14
|
||||
# (soms2_52) nur bei Frauen anwendbar.
|
||||
SOMS2_ICD10_EINHEITEN = list(
|
||||
"soms2_02",
|
||||
c("soms2_04", "soms2_05"),
|
||||
"soms2_06",
|
||||
c("soms2_09", "soms2_22", "soms2_38"),
|
||||
"soms2_10",
|
||||
"soms2_11",
|
||||
c("soms2_13", "soms2_14"),
|
||||
"soms2_18",
|
||||
c("soms2_20", "soms2_21"),
|
||||
"soms2_28",
|
||||
"soms2_31",
|
||||
"soms2_33",
|
||||
c("soms2_40", "soms2_41"),
|
||||
"soms2_52"
|
||||
)
|
||||
|
||||
# SAD-Index ICD-10: 12 Wertungseinheiten, keine Geschlechtertrennung.
|
||||
SOMS2_SAD_EINHEITEN = list(
|
||||
c("soms2_06", "soms2_25"),
|
||||
"soms2_11",
|
||||
"soms2_12",
|
||||
"soms2_15",
|
||||
"soms2_19",
|
||||
c("soms2_09", "soms2_22", "soms2_38"),
|
||||
"soms2_23",
|
||||
"soms2_24",
|
||||
"soms2_26",
|
||||
"soms2_27",
|
||||
c("soms2_28", "soms2_29"),
|
||||
"soms2_30"
|
||||
)
|
||||
|
||||
# Beschwerdenindex Somatisierung: alle Items soms2_01-soms2_53, keine Auswahl.
|
||||
SOMS2_BESCHWERDEN_ITEMS = sprintf("soms2_%02d", 1:53)
|
||||
|
||||
# Zusatz-Screeningitems 64-68: 65/67 sind Folgefragen zu 64/66, 68 steht
|
||||
# alleine. Reine Einzelitem-Anzeige, kein Score.
|
||||
SOMS2_ZUSATZ_ITEMS = list(
|
||||
list(haupt = "soms2_64", folge = "soms2_65"),
|
||||
list(haupt = "soms2_66", folge = "soms2_67"),
|
||||
list(haupt = "soms2_68", folge = NULL)
|
||||
)
|
||||
|
||||
# Spaltenzuordnung fuer die Prozentrang-Lookups je Normtabelle.
|
||||
SOMS2_NORM_SPALTEN_A1 = list(
|
||||
dsmiv = list(gesamt = "dsmiv_gesamt", m = "dsmiv_m", w = "dsmiv_w"),
|
||||
icd10 = list(gesamt = "icd10_gesamt", m = "icd10_m", w = "icd10_w"),
|
||||
sad = list(gesamt = "sad_gesamt", m = "sad_m", w = "sad_w"),
|
||||
beschwerden = list(gesamt = "beschwerdenindex_gesamt", m = "beschwerdenindex_m", w = "beschwerdenindex_w")
|
||||
)
|
||||
SOMS2_NORM_SPALTEN_A2 = list(
|
||||
dsmiv = "dsmiv",
|
||||
icd10 = "icd10",
|
||||
sad = "sad",
|
||||
beschwerden = "beschwerdenindex_gesamt"
|
||||
)
|
||||
|
||||
|
||||
# UI ####
|
||||
|
||||
app_css = "
|
||||
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f5f5f5; }
|
||||
.app-header {
|
||||
background: #8B2635; color: white; padding: 18px 24px 14px;
|
||||
margin-bottom: 20px; border-radius: 0 0 6px 6px;
|
||||
}
|
||||
.app-header h2 { margin: 0; font-size: 1.5rem; font-weight: 600; }
|
||||
.app-header p { margin: 4px 0 0; opacity: 0.85; font-size: 0.9rem; }
|
||||
.input-panel {
|
||||
background: white; border-radius: 6px; padding: 16px 20px;
|
||||
margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.12);
|
||||
display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap;
|
||||
}
|
||||
.input-panel .form-group { margin-bottom: 0; }
|
||||
.input-panel label { font-weight: 600; color: #333; }
|
||||
.btn-laden {
|
||||
background: #8B2635 !important; color: white !important;
|
||||
border: none !important; border-radius: 4px !important;
|
||||
padding: 8px 20px !important; font-weight: 600 !important; cursor: pointer;
|
||||
}
|
||||
.btn-laden:hover { background: #6d1e29 !important; }
|
||||
.alert-fehler {
|
||||
background: #FFEBEE; border-left: 5px solid #C62828;
|
||||
padding: 12px 16px; border-radius: 4px; color: #B71C1C;
|
||||
margin-bottom: 12px; font-weight: 500;
|
||||
}
|
||||
.alert-warnung {
|
||||
background: #FFF3E0; border-left: 5px solid #E65100;
|
||||
padding: 10px 16px; border-radius: 4px; color: #BF360C;
|
||||
margin-bottom: 12px; font-size: 0.93em; font-weight: 500;
|
||||
}
|
||||
.abschnitt-karte {
|
||||
background: white; border-radius: 6px; padding: 20px 24px;
|
||||
margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.12);
|
||||
}
|
||||
.abschnitt-titel {
|
||||
color: #8B2635; font-size: 1.15rem; font-weight: 700;
|
||||
border-bottom: 2px solid #8B2635; padding-bottom: 8px; margin-bottom: 14px;
|
||||
}
|
||||
.meta-block { margin-bottom: 10px; color: #555; font-size: 0.95em; }
|
||||
.meta-block strong { color: #222; }
|
||||
.item-zeile {
|
||||
display: flex; align-items: flex-start; gap: 10px;
|
||||
padding: 7px 0; border-bottom: 1px solid #F0F0F0;
|
||||
}
|
||||
.item-zeile.item-unterpunkt { padding-left: 28px; border-bottom: none; }
|
||||
.item-nr { font-weight: 600; color: #8B2635; min-width: 34px; flex-shrink: 0; }
|
||||
.item-text { flex: 1; color: #333; font-size: 0.92em; }
|
||||
.kriterien-liste { margin: 10px 0; padding: 0; list-style: none; }
|
||||
.kriterium-zeile {
|
||||
display: flex; align-items: flex-start; gap: 10px; padding: 8px 0;
|
||||
border-bottom: 1px solid #F5F5F5; font-size: 0.92em; color: #444;
|
||||
}
|
||||
.kriterium-status { font-weight: 700; min-width: 100px; flex-shrink: 0; }
|
||||
.kriterium-erfuellt { color: #2E7D32; }
|
||||
.kriterium-nicht-erfuellt { color: #B71C1C; }
|
||||
.kriterium-inhalt { flex: 1; display: flex; flex-direction: column; gap: 2px; }
|
||||
.kriterium-frage { color: #333; }
|
||||
.kriterium-antwort { color: #777; font-size: 0.85em; }
|
||||
.kriterium-verknuepfung {
|
||||
font-size: 0.78em; color: #999; font-style: italic; margin: 2px 0;
|
||||
}
|
||||
.gesamtstatus-box {
|
||||
border-radius: 6px; padding: 12px 16px; margin-top: 10px;
|
||||
font-weight: 700; border-left: 5px solid;
|
||||
}
|
||||
.gesamtstatus-erfuellt { background: #E8F5E9; border-color: #2E7D32; color: #2E7D32; }
|
||||
.gesamtstatus-nicht-erfuellt { background: #F5F5F5; border-color: #9E9E9E; color: #616161; }
|
||||
.score-zahl { font-size: 2.2rem; font-weight: 800; color: #8B2635; }
|
||||
.score-label { color: #555; font-size: 0.9em; }
|
||||
.pr-info { color: #555; font-size: 0.9em; margin-top: 4px; }
|
||||
.cutoff-hinweis {
|
||||
background: #FFF3E0; border-left: 5px solid #E65100; color: #BF360C;
|
||||
padding: 10px 14px; border-radius: 4px; margin-top: 10px; font-size: 0.9em;
|
||||
}
|
||||
.normwahl-hinweis { color: #777; font-size: 0.82em; font-style: italic; margin-top: 6px; }
|
||||
.start-hinweis { text-align: center; color: #bbb; padding: 30px 0; font-style: italic; }
|
||||
"
|
||||
|
||||
app_css = gsub("#8B2635", AKZENT_FARBE, app_css, fixed = TRUE)
|
||||
|
||||
ui = fluidPage(
|
||||
tags$head(
|
||||
tags$meta(charset = "UTF-8"),
|
||||
tags$style(HTML(app_css))
|
||||
),
|
||||
|
||||
div(class = "app-header",
|
||||
tags$h2("SOMS-2 - Screening fuer somatoforme Stoerungen (2-Jahres-Version)"),
|
||||
tags$p("Rief, Hiller & Heuser 1997 | Vier parallele Auswertungskennwerte")
|
||||
),
|
||||
|
||||
div(class = "container-fluid",
|
||||
|
||||
div(class = "input-panel",
|
||||
div(style = "min-width: 360px; white-space: nowrap;",
|
||||
textInput("pseudonym",
|
||||
label = tagList(
|
||||
"Pseudonym",
|
||||
tags$span(style = "font-weight: normal; font-style: italic; font-size: 0.78em; color: #888; margin-left: 4px; white-space: nowrap;",
|
||||
"optional, hat Vorrang vor Chiffre")
|
||||
),
|
||||
placeholder = "optional", width = "340px")
|
||||
),
|
||||
div(style = "min-width: 200px;",
|
||||
textInput("chiffre", label = "Patientenchiffre",
|
||||
placeholder = "z.B. P000123", width = "100%")
|
||||
),
|
||||
actionButton("btn_suchen", "Auswerten", class = "btn btn-primary btn-laden"),
|
||||
div(style = "margin-left: auto;",
|
||||
downloadButton("download_word", "Word-Export (.docx)")
|
||||
)
|
||||
),
|
||||
|
||||
div(class = "input-panel",
|
||||
div(
|
||||
tags$label("Vergleichsnorm",
|
||||
style = "font-weight:600; color:#333; display:block; margin-bottom:4px;"),
|
||||
radioButtons("normwahl", label = NULL,
|
||||
choices = c("Gesunde" = "gesunde", "psychosomatische Patienten" = "patienten"),
|
||||
selected = "gesunde", inline = TRUE)
|
||||
),
|
||||
uiOutput("normwahl_hinweis_ui")
|
||||
),
|
||||
|
||||
uiOutput("ergebnis_ui")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Word-Export ####
|
||||
|
||||
erstelle_soms2_docx = function(erg) {
|
||||
doc = read_docx()
|
||||
|
||||
fp_titel = fp_text(color = AKZENT_FARBE, bold = TRUE, font.size = 18)
|
||||
fp_abschnitt = fp_text(color = AKZENT_FARBE, bold = TRUE, font.size = 13)
|
||||
fp_label = fp_text(bold = TRUE, font.size = 11)
|
||||
fp_normal = fp_text(font.size = 11)
|
||||
fp_erfuellt = fp_text(font.size = 10, bold = TRUE, color = "#2E7D32")
|
||||
fp_nicht = fp_text(font.size = 10, bold = TRUE, color = "#B71C1C")
|
||||
fp_gesamt_ok = fp_text(font.size = 11, bold = TRUE, color = "#2E7D32")
|
||||
fp_gesamt_nok = fp_text(font.size = 11, bold = TRUE, color = "#616161")
|
||||
fp_cutoff = fp_text(font.size = 10, italic = TRUE, color = "#BF360C")
|
||||
fp_warnung = fp_text(font.size = 10, italic = TRUE, color = "#8a6d00")
|
||||
fp_normhinweis = fp_text(font.size = 9.5, italic = TRUE, color = "#555555")
|
||||
fp_antwort = fp_text(font.size = 9.5, color = "#777777")
|
||||
fp_verknuepfung = fp_text(font.size = 9, italic = TRUE, color = "#999999")
|
||||
fp_disclaimer = fp_text(font.size = 9, italic = TRUE, color = "#777777")
|
||||
|
||||
doc = body_add_fpar(doc, fpar(ftext("SOMS-2 - Auswertung", fp_titel)))
|
||||
doc = body_add_fpar(doc, fpar(
|
||||
ftext("Chiffre: ", fp_label),
|
||||
ftext(erg$chiffre, fp_normal),
|
||||
ftext(" Ausfuelldatum: ", fp_label),
|
||||
ftext(erg$datum_str, fp_normal),
|
||||
ftext(" Geschlecht: ", fp_label),
|
||||
ftext(if (is.na(erg$geschlecht_text)) "k. A." else erg$geschlecht_text, fp_normal)
|
||||
))
|
||||
if (!is.null(erg$warnung_daten)) {
|
||||
doc = body_add_fpar(doc, fpar(ftext(erg$warnung_daten, fp_warnung)))
|
||||
}
|
||||
doc = body_add_fpar(doc, fpar(ftext(
|
||||
if (identical(erg$normwahl, "patienten"))
|
||||
"Vergleichsnorm: psychosomatische Patienten (nicht nach Geschlecht getrennt)"
|
||||
else
|
||||
"Vergleichsnorm: Gesunde",
|
||||
fp_normhinweis
|
||||
)))
|
||||
doc = body_add_par(doc, "", style = "Normal")
|
||||
|
||||
for (idx in erg$indizes) {
|
||||
doc = body_add_fpar(doc, fpar(ftext(idx$titel, fp_abschnitt)))
|
||||
doc = body_add_fpar(doc, fpar(
|
||||
ftext("Rohwert: ", fp_label),
|
||||
ftext(paste0(idx$rohwert, " / ", idx$max), fp_normal),
|
||||
ftext(" Prozentrang: ", fp_label),
|
||||
ftext(if (is.na(idx$pr)) "k. A." else paste0(idx$pr), fp_normal)
|
||||
))
|
||||
if (!is.null(idx$kriterien)) {
|
||||
for (k in idx$kriterien) {
|
||||
status_txt = if (isTRUE(k$erfuellt)) "[erfuellt] " else "[nicht erfuellt] "
|
||||
status_fp = if (isTRUE(k$erfuellt)) fp_erfuellt else fp_nicht
|
||||
for (i in seq_along(k$teile)) {
|
||||
teil = k$teile[[i]]
|
||||
if (i == 1) {
|
||||
doc = body_add_fpar(doc, fpar(ftext(status_txt, status_fp), ftext(teil$text, fp_normal)))
|
||||
} else {
|
||||
doc = body_add_fpar(doc, fpar(
|
||||
ftext(paste0(" ", k$verknuepfung, " "), fp_verknuepfung),
|
||||
ftext(teil$text, fp_normal)
|
||||
))
|
||||
}
|
||||
doc = body_add_fpar(doc, fpar(ftext(
|
||||
paste0(" Antwort: ", teil$antwort, " (erforderlich: ", teil$erforderlich, ")"),
|
||||
fp_antwort
|
||||
)))
|
||||
}
|
||||
}
|
||||
doc = body_add_fpar(doc, fpar(ftext(
|
||||
if (isTRUE(idx$gesamtstatus)) "Kriterien fuer Verdachtsdiagnose erfuellt"
|
||||
else "Kriterien fuer Verdachtsdiagnose nicht (vollstaendig) erfuellt",
|
||||
if (isTRUE(idx$gesamtstatus)) fp_gesamt_ok else fp_gesamt_nok
|
||||
)))
|
||||
}
|
||||
if (!is.null(idx$cutoff_hinweis)) {
|
||||
doc = body_add_fpar(doc, fpar(ftext(idx$cutoff_hinweis, fp_cutoff)))
|
||||
}
|
||||
if (identical(idx$key, "beschwerden")) {
|
||||
if (length(erg$beschwerden_ja_items) > 0) {
|
||||
doc = body_add_fpar(doc, fpar(ftext("Angegebene Beschwerden (mit 'ja' beantwortet):", fp_label)))
|
||||
for (b in erg$beschwerden_ja_items) {
|
||||
doc = body_add_fpar(doc, fpar(ftext(paste0(b$nr, ". ", b$text), fp_normal)))
|
||||
}
|
||||
} else {
|
||||
doc = body_add_fpar(doc, fpar(ftext("Keine der 53 Beschwerden wurde mit 'ja' beantwortet.", fp_normal)))
|
||||
}
|
||||
}
|
||||
doc = body_add_par(doc, "", style = "Normal")
|
||||
}
|
||||
|
||||
if (length(erg$screening_hinweise) > 0) {
|
||||
doc = body_add_fpar(doc, fpar(ftext("Weitere Screening-Hinweise", fp_abschnitt)))
|
||||
for (h in erg$screening_hinweise) {
|
||||
doc = body_add_fpar(doc, fpar(ftext(paste0(h$nr, ". ", h$text), fp_normal)))
|
||||
if (!is.null(h$folge_text)) {
|
||||
doc = body_add_fpar(doc, fpar(ftext(paste0(" -> ", h$folge_text), fp_normal)))
|
||||
}
|
||||
}
|
||||
doc = body_add_par(doc, "", style = "Normal")
|
||||
}
|
||||
|
||||
doc = body_add_fpar(doc, fpar(ftext(SOMS2_DISCLAIMER, fp_disclaimer)))
|
||||
|
||||
doc
|
||||
}
|
||||
|
||||
|
||||
# Server ####
|
||||
|
||||
server = function(input, output, session) {
|
||||
|
||||
observe({
|
||||
query = parseQueryString(session$clientData$url_search)
|
||||
if (!is.null(query$pseudonym) && nchar(trimws(query$pseudonym)) > 0) {
|
||||
updateTextInput(session, "pseudonym", value = trimws(query$pseudonym))
|
||||
}
|
||||
})
|
||||
|
||||
observe({
|
||||
query = parseQueryString(session$clientData$url_search)
|
||||
if (!is.null(query$chiffre) && nchar(trimws(query$chiffre)) > 0) {
|
||||
updateTextInput(session, "chiffre", value = toupper(trimws(query$chiffre)))
|
||||
}
|
||||
})
|
||||
|
||||
rohdaten_r = eventReactive(input$btn_suchen, {
|
||||
|
||||
chiffre = toupper(trimws(input$chiffre))
|
||||
|
||||
if (nchar(trimws(input$pseudonym)) == 0 && nchar(chiffre) == 0) {
|
||||
return(list(typ = "leere_eingabe", meldung = "Bitte Chiffre oder Pseudonym eingeben."))
|
||||
}
|
||||
if (nchar(trimws(input$pseudonym)) == 0 && !grepl("^[A-Z][0-9]{6}$", chiffre)) {
|
||||
return(list(typ = "format_fehler", chiffre = chiffre))
|
||||
}
|
||||
|
||||
if (!file.exists(PFAD_DOWNLOAD_SKRIPT)) {
|
||||
return(list(typ = "skript_fehler",
|
||||
meldung = paste0("Download-Skript nicht gefunden: ", PFAD_DOWNLOAD_SKRIPT)))
|
||||
}
|
||||
if (!file.exists(PFAD_PSEUDONYM_SKRIPT)) {
|
||||
return(list(typ = "skript_fehler",
|
||||
meldung = paste0("Pseudonym-Skript nicht gefunden: ", PFAD_PSEUDONYM_SKRIPT)))
|
||||
}
|
||||
|
||||
ok = tryCatch({
|
||||
source(PFAD_DOWNLOAD_SKRIPT, local = FALSE)
|
||||
list(ok = TRUE)
|
||||
}, error = function(e) list(ok = FALSE, msg = e$message))
|
||||
if (!ok$ok) return(list(typ = "skript_fehler", meldung = ok$msg))
|
||||
|
||||
db_ordner = local({
|
||||
ordner = dirname(normalizePath(PFAD_PSEUDONYM_SKRIPT, mustWork = FALSE))
|
||||
gefunden = NULL
|
||||
for (i in 1:5) {
|
||||
if (file.exists(file.path(ordner, "pseudonyme.db"))) {
|
||||
gefunden = ordner
|
||||
break
|
||||
}
|
||||
elternteil = dirname(ordner)
|
||||
if (elternteil == ordner) break
|
||||
ordner = elternteil
|
||||
}
|
||||
gefunden
|
||||
})
|
||||
if (is.null(db_ordner)) {
|
||||
return(list(typ = "skript_fehler",
|
||||
meldung = "pseudonyme.db wurde ausgehend vom Pseudonym-Skript-Ordner bis zu 5 Ebenen nach oben nicht gefunden."))
|
||||
}
|
||||
|
||||
alter_wd = getwd()
|
||||
on.exit(setwd(alter_wd), add = TRUE)
|
||||
setwd(db_ordner)
|
||||
|
||||
ok_ps = tryCatch({
|
||||
source(PFAD_PSEUDONYM_SKRIPT, local = FALSE)
|
||||
list(ok = TRUE)
|
||||
}, error = function(e) list(ok = FALSE, msg = e$message))
|
||||
if (!ok_ps$ok) return(list(typ = "skript_fehler", meldung = ok_ps$msg))
|
||||
|
||||
if (!exists("daten_soms2", envir = .GlobalEnv)) {
|
||||
return(list(typ = "skript_fehler",
|
||||
meldung = "Objekt 'daten_soms2' wurde nach dem Sourcen des Download-Skripts nicht gefunden."))
|
||||
}
|
||||
if (!exists("pseudo", envir = .GlobalEnv)) {
|
||||
return(list(typ = "skript_fehler",
|
||||
meldung = "Objekt 'pseudo' wurde nach dem Sourcen des Pseudonym-Skripts nicht gefunden."))
|
||||
}
|
||||
|
||||
daten_soms2 = get("daten_soms2", envir = .GlobalEnv)
|
||||
pseudo = get("pseudo", envir = .GlobalEnv)
|
||||
|
||||
if (nchar(trimws(input$pseudonym)) > 0) {
|
||||
pw_treffer = pseudo[pseudo$pseudonym == trimws(input$pseudonym), ]
|
||||
if (nrow(pw_treffer) > 0) chiffre = toupper(trimws(pw_treffer$chiffre[1]))
|
||||
}
|
||||
|
||||
treffer_ps = pseudo[pseudo$chiffre == chiffre, ]
|
||||
if (nrow(treffer_ps) == 0) {
|
||||
return(list(typ = "chiffre_nicht_gefunden", chiffre = chiffre))
|
||||
}
|
||||
|
||||
alle_session_ids = unique(treffer_ps$pseudonym)
|
||||
if (nchar(trimws(input$pseudonym)) > 0) alle_session_ids = trimws(input$pseudonym)
|
||||
|
||||
treffer_dat = daten_soms2[daten_soms2$session %in% alle_session_ids, ]
|
||||
if (nrow(treffer_dat) == 0) {
|
||||
return(list(typ = "session_nicht_gefunden", chiffre = chiffre))
|
||||
}
|
||||
|
||||
warnung_daten = NULL
|
||||
if (nrow(treffer_dat) > 1) {
|
||||
n = nrow(treffer_dat)
|
||||
treffer_dat = treffer_dat[order(treffer_dat$created, decreasing = TRUE), ]
|
||||
datum_neu = tryCatch(
|
||||
format(as.POSIXct(treffer_dat$created[1]), "%d.%m.%Y %H:%M"),
|
||||
error = function(e) "unbekanntes Datum"
|
||||
)
|
||||
warnung_daten = paste0(
|
||||
"Mehrere Ausfuellungen gefunden (", n, " Eintraege). ",
|
||||
"Angezeigt wird die neueste vom ", datum_neu, "."
|
||||
)
|
||||
treffer_dat = treffer_dat[1, , drop = FALSE]
|
||||
}
|
||||
|
||||
zeile = treffer_dat[1, , drop = FALSE]
|
||||
datum_str = tryCatch(
|
||||
format(as.POSIXct(zeile[["created"]][1]), "%d.%m.%Y"),
|
||||
error = function(e) format(Sys.Date(), "%d.%m.%Y")
|
||||
)
|
||||
|
||||
geschlecht_text = soms2_geschlecht_text(daten_soms2[["soms2_geschlecht"]], zeile[["soms2_geschlecht"]])
|
||||
|
||||
dsmiv_rohwert = soms2_score_items(zeile, daten_soms2, SOMS2_DSMIV_ITEMS)
|
||||
dsmiv_max = soms2_dsmiv_max(geschlecht_text)
|
||||
dsmiv_krit = soms2_dsmiv_kriterien(zeile, daten_soms2)
|
||||
|
||||
icd10_rohwert = soms2_score_einheiten(zeile, daten_soms2, SOMS2_ICD10_EINHEITEN)
|
||||
icd10_max = soms2_icd10_max(geschlecht_text)
|
||||
icd10_krit = soms2_icd10_kriterien(zeile, daten_soms2)
|
||||
|
||||
sad_rohwert = soms2_score_einheiten(zeile, daten_soms2, SOMS2_SAD_EINHEITEN)
|
||||
sad_max = 12L
|
||||
sad_krit = soms2_sad_kriterien(zeile, daten_soms2)
|
||||
|
||||
beschwerden_rohwert = soms2_score_items(zeile, daten_soms2, SOMS2_BESCHWERDEN_ITEMS)
|
||||
beschwerden_max = soms2_beschwerden_max(geschlecht_text)
|
||||
|
||||
# Liste der einzelnen mit "ja" beantworteten Beschwerden (Items 1-53) fuer
|
||||
# die Anzeige unter dem Beschwerdenindex.
|
||||
beschwerden_ja_items = list()
|
||||
for (it in SOMS2_BESCHWERDEN_ITEMS) {
|
||||
if (isTRUE(soms2_ist_ja(daten_soms2[[it]], zeile[[it]]))) {
|
||||
beschwerden_ja_items[[length(beschwerden_ja_items) + 1]] = list(
|
||||
nr = as.integer(sub("^soms2_", "", it)),
|
||||
text = soms2_item_text(daten_soms2, it)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
screening_hinweise = list()
|
||||
for (paar in SOMS2_ZUSATZ_ITEMS) {
|
||||
if (isTRUE(soms2_ist_ja(daten_soms2[[paar$haupt]], zeile[[paar$haupt]]))) {
|
||||
folge_text = NULL
|
||||
if (!is.null(paar$folge) && isTRUE(soms2_ist_ja(daten_soms2[[paar$folge]], zeile[[paar$folge]]))) {
|
||||
folge_text = soms2_item_text(daten_soms2, paar$folge)
|
||||
}
|
||||
screening_hinweise[[length(screening_hinweise) + 1]] = list(
|
||||
nr = as.integer(sub("^soms2_", "", paar$haupt)),
|
||||
text = soms2_item_text(daten_soms2, paar$haupt),
|
||||
folge_text = folge_text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
list(
|
||||
typ = "ergebnis",
|
||||
chiffre = chiffre,
|
||||
datum_str = datum_str,
|
||||
warnung_daten = warnung_daten,
|
||||
geschlecht_text = geschlecht_text,
|
||||
dsmiv_rohwert = dsmiv_rohwert, dsmiv_max = dsmiv_max, dsmiv_krit = dsmiv_krit,
|
||||
icd10_rohwert = icd10_rohwert, icd10_max = icd10_max, icd10_krit = icd10_krit,
|
||||
sad_rohwert = sad_rohwert, sad_max = sad_max, sad_krit = sad_krit,
|
||||
beschwerden_rohwert = beschwerden_rohwert, beschwerden_max = beschwerden_max,
|
||||
beschwerden_ja_items = beschwerden_ja_items,
|
||||
screening_hinweise = screening_hinweise
|
||||
)
|
||||
})
|
||||
|
||||
# Leichtgewichtiges reactive() statt eventReactive: die Prozentraenge sollen
|
||||
# sich sofort aktualisieren, wenn die Vergleichsnorm umgeschaltet wird, ohne
|
||||
# dass erneut "Auswerten" geklickt werden muss (Rohdaten/Kriterien bleiben
|
||||
# dabei unveraendert, nur der Normtabellen-Lookup wird neu berechnet).
|
||||
ergebnis_r = reactive({
|
||||
d = rohdaten_r()
|
||||
if (d$typ != "ergebnis") return(d)
|
||||
|
||||
normwahl = input$normwahl
|
||||
if (is.null(normwahl)) normwahl = "gesunde"
|
||||
|
||||
dsmiv_pr = soms2_pr_ergebnis("dsmiv", d$dsmiv_rohwert, d$geschlecht_text, normwahl, soms2_norm_a1, soms2_norm_a2)
|
||||
icd10_pr = soms2_pr_ergebnis("icd10", d$icd10_rohwert, d$geschlecht_text, normwahl, soms2_norm_a1, soms2_norm_a2)
|
||||
sad_pr = soms2_pr_ergebnis("sad", d$sad_rohwert, d$geschlecht_text, normwahl, soms2_norm_a1, soms2_norm_a2)
|
||||
besch_pr = soms2_pr_ergebnis("beschwerden", d$beschwerden_rohwert, d$geschlecht_text, normwahl, soms2_norm_a1, soms2_norm_a2)
|
||||
|
||||
indizes = list(
|
||||
list(
|
||||
key = "dsmiv", titel = "Somatisierungsindex DSM-IV",
|
||||
rohwert = d$dsmiv_rohwert, max = d$dsmiv_max,
|
||||
pr = dsmiv_pr$pr, pr_gesamt = dsmiv_pr$pr_gesamt,
|
||||
kriterien = d$dsmiv_krit,
|
||||
gesamtstatus = all(vapply(d$dsmiv_krit, function(k) isTRUE(k$erfuellt), logical(1))),
|
||||
cutoff_hinweis = NULL
|
||||
),
|
||||
list(
|
||||
key = "icd10", titel = "Somatisierungsindex ICD-10",
|
||||
rohwert = d$icd10_rohwert, max = d$icd10_max,
|
||||
pr = icd10_pr$pr, pr_gesamt = icd10_pr$pr_gesamt,
|
||||
kriterien = d$icd10_krit,
|
||||
gesamtstatus = all(vapply(d$icd10_krit, function(k) isTRUE(k$erfuellt), logical(1))),
|
||||
cutoff_hinweis = NULL
|
||||
),
|
||||
list(
|
||||
key = "sad", titel = "SAD-Index ICD-10",
|
||||
rohwert = d$sad_rohwert, max = d$sad_max,
|
||||
pr = sad_pr$pr, pr_gesamt = sad_pr$pr_gesamt,
|
||||
kriterien = d$sad_krit,
|
||||
gesamtstatus = all(vapply(d$sad_krit, function(k) isTRUE(k$erfuellt), logical(1))),
|
||||
cutoff_hinweis = NULL
|
||||
),
|
||||
list(
|
||||
key = "beschwerden", titel = "Beschwerdenindex Somatisierung",
|
||||
rohwert = d$beschwerden_rohwert, max = d$beschwerden_max,
|
||||
pr = besch_pr$pr, pr_gesamt = besch_pr$pr_gesamt,
|
||||
kriterien = NULL,
|
||||
gesamtstatus = NA,
|
||||
cutoff_hinweis = if (isTRUE(d$beschwerden_rohwert >= 7)) SOMS2_BESCHWERDEN_CUTOFF_HINWEIS else NULL
|
||||
)
|
||||
)
|
||||
|
||||
c(d, list(normwahl = normwahl, indizes = indizes))
|
||||
})
|
||||
|
||||
output$normwahl_hinweis_ui = renderUI({
|
||||
if (identical(input$normwahl, "patienten")) {
|
||||
div(class = "normwahl-hinweis",
|
||||
"Die Patientennorm liegt nicht nach Geschlecht getrennt vor (eine Vergleichsgruppe fuer alle).")
|
||||
} else {
|
||||
NULL
|
||||
}
|
||||
})
|
||||
|
||||
output$ergebnis_ui = renderUI({
|
||||
|
||||
if (input$btn_suchen == 0) {
|
||||
return(div(class = "abschnitt-karte start-hinweis",
|
||||
"Bitte Chiffre oder Pseudonym eingeben und auf 'Auswerten' klicken."
|
||||
))
|
||||
}
|
||||
|
||||
erg = ergebnis_r()
|
||||
|
||||
if (erg$typ == "leere_eingabe") {
|
||||
return(div(class = "alert-warnung", erg$meldung))
|
||||
}
|
||||
if (erg$typ == "format_fehler") {
|
||||
return(div(class = "alert-warnung",
|
||||
paste0("Ungueltige Chiffre '", erg$chiffre, "'. Erwartet: ein Grossbuchstabe gefolgt von 6 Ziffern (z. B. P000123).")))
|
||||
}
|
||||
if (erg$typ == "skript_fehler") {
|
||||
return(div(class = "alert-fehler", tags$pre(style = "white-space:pre-wrap; margin:0;", erg$meldung)))
|
||||
}
|
||||
if (erg$typ == "chiffre_nicht_gefunden") {
|
||||
return(div(class = "alert-fehler",
|
||||
paste0("Chiffre '", erg$chiffre, "' wurde in der Pseudonym-Datenbank nicht gefunden.")))
|
||||
}
|
||||
if (erg$typ == "session_nicht_gefunden") {
|
||||
return(div(class = "alert-fehler",
|
||||
paste0("Kein SOMS-2-Datensatz fuer Chiffre '", erg$chiffre, "' gefunden.")))
|
||||
}
|
||||
|
||||
meta_block = div(class = "meta-block",
|
||||
tags$strong("Chiffre: "), erg$chiffre, " ",
|
||||
tags$strong("Ausfuelldatum: "), erg$datum_str, " ",
|
||||
tags$strong("Geschlecht: "), if (is.na(erg$geschlecht_text)) "k. A." else erg$geschlecht_text
|
||||
)
|
||||
|
||||
kopf_karte = div(class = "abschnitt-karte",
|
||||
meta_block,
|
||||
if (!is.null(erg$warnung_daten)) div(class = "alert-warnung", erg$warnung_daten) else NULL
|
||||
)
|
||||
|
||||
index_karten = lapply(erg$indizes, function(idx) {
|
||||
|
||||
pr_text = if (is.na(idx$pr)) "k. A." else paste0(idx$pr)
|
||||
pr_zusatz = if (!identical(erg$normwahl, "patienten") &&
|
||||
!is.na(erg$geschlecht_text) &&
|
||||
!is.na(idx$pr_gesamt))
|
||||
paste0(" (Gesamtnorm: ", idx$pr_gesamt, ")") else ""
|
||||
|
||||
kriterien_ui = if (!is.null(idx$kriterien)) {
|
||||
tagList(
|
||||
div(class = "kriterien-liste",
|
||||
lapply(idx$kriterien, function(k) {
|
||||
teile_ui = lapply(seq_along(k$teile), function(i) {
|
||||
teil = k$teile[[i]]
|
||||
tagList(
|
||||
if (i > 1) div(class = "kriterium-verknuepfung", k$verknuepfung) else NULL,
|
||||
div(class = "kriterium-frage", teil$text),
|
||||
div(class = "kriterium-antwort",
|
||||
paste0("Antwort: ", teil$antwort, " (erforderlich: ", teil$erforderlich, ")"))
|
||||
)
|
||||
})
|
||||
div(class = "kriterium-zeile",
|
||||
span(class = paste0("kriterium-status ",
|
||||
if (isTRUE(k$erfuellt)) "kriterium-erfuellt" else "kriterium-nicht-erfuellt"),
|
||||
if (isTRUE(k$erfuellt)) "erfuellt" else "nicht erfuellt"),
|
||||
div(class = "kriterium-inhalt", teile_ui)
|
||||
)
|
||||
})
|
||||
),
|
||||
div(class = paste0("gesamtstatus-box ",
|
||||
if (isTRUE(idx$gesamtstatus)) "gesamtstatus-erfuellt" else "gesamtstatus-nicht-erfuellt"),
|
||||
if (isTRUE(idx$gesamtstatus)) "Kriterien fuer Verdachtsdiagnose erfuellt"
|
||||
else "Kriterien fuer Verdachtsdiagnose nicht (vollstaendig) erfuellt")
|
||||
)
|
||||
} else NULL
|
||||
|
||||
cutoff_ui = if (!is.null(idx$cutoff_hinweis)) div(class = "cutoff-hinweis", idx$cutoff_hinweis) else NULL
|
||||
|
||||
beschwerden_liste_ui = if (identical(idx$key, "beschwerden")) {
|
||||
if (length(erg$beschwerden_ja_items) > 0) {
|
||||
tagList(
|
||||
tags$h5("Angegebene Beschwerden (mit 'ja' beantwortet)",
|
||||
style = "margin-top:14px; margin-bottom:6px; color:#555; font-size:0.95em;"),
|
||||
lapply(erg$beschwerden_ja_items, function(b) {
|
||||
div(class = "item-zeile",
|
||||
div(class = "item-nr", paste0(b$nr, ".")),
|
||||
div(class = "item-text", b$text)
|
||||
)
|
||||
})
|
||||
)
|
||||
} else {
|
||||
div(class = "normwahl-hinweis",
|
||||
"Keine der 53 Beschwerden wurde mit 'ja' beantwortet.")
|
||||
}
|
||||
} else NULL
|
||||
|
||||
div(class = "abschnitt-karte",
|
||||
div(class = "abschnitt-titel", idx$titel),
|
||||
div(
|
||||
span(class = "score-zahl", idx$rohwert),
|
||||
span(class = "score-label", paste0(" / ", idx$max))
|
||||
),
|
||||
div(class = "pr-info", paste0("Prozentrang: ", pr_text, pr_zusatz)),
|
||||
kriterien_ui,
|
||||
cutoff_ui,
|
||||
beschwerden_liste_ui
|
||||
)
|
||||
})
|
||||
|
||||
hinweise_karte = if (length(erg$screening_hinweise) > 0) {
|
||||
div(class = "abschnitt-karte",
|
||||
div(class = "abschnitt-titel", "Weitere Screening-Hinweise"),
|
||||
lapply(erg$screening_hinweise, function(h) {
|
||||
tagList(
|
||||
div(class = "item-zeile",
|
||||
div(class = "item-nr", paste0(h$nr, ".")),
|
||||
div(class = "item-text", h$text)
|
||||
),
|
||||
if (!is.null(h$folge_text))
|
||||
div(class = "item-zeile item-unterpunkt",
|
||||
div(class = "item-text", h$folge_text))
|
||||
else NULL
|
||||
)
|
||||
})
|
||||
)
|
||||
} else NULL
|
||||
|
||||
tagList(kopf_karte, index_karten, hinweise_karte)
|
||||
})
|
||||
|
||||
output$download_word = downloadHandler(
|
||||
filename = function() {
|
||||
erg = tryCatch(ergebnis_r(), error = function(e) NULL)
|
||||
if (is.null(erg) || erg$typ != "ergebnis") return("SOMS2_Export.docx")
|
||||
chiffre_esc = gsub("[^A-Za-z0-9_-]", "_", erg$chiffre)
|
||||
ausfuelldatum_fn = tryCatch(
|
||||
format(as.Date(erg$datum_str, "%d.%m.%Y"), "%Y%m%d"),
|
||||
error = function(e) format(Sys.Date(), "%Y%m%d")
|
||||
)
|
||||
paste0("SOMS2_", chiffre_esc, "_", ausfuelldatum_fn, ".docx")
|
||||
},
|
||||
content = function(file) {
|
||||
erg = tryCatch(ergebnis_r(), error = function(e) NULL)
|
||||
if (is.null(erg) || erg$typ != "ergebnis") {
|
||||
doc = read_docx()
|
||||
doc = body_add_par(doc,
|
||||
"Kein Datensatz geladen. Bitte zuerst Chiffre oder Pseudonym eingeben und 'Auswerten' klicken.",
|
||||
style = "Normal")
|
||||
print(doc, target = file)
|
||||
return()
|
||||
}
|
||||
doc = tryCatch(
|
||||
erstelle_soms2_docx(erg),
|
||||
error = function(e) {
|
||||
err_doc = read_docx()
|
||||
body_add_par(err_doc,
|
||||
paste0("Fehler beim Erstellen des Word-Dokuments: ", e$message),
|
||||
style = "Normal")
|
||||
}
|
||||
)
|
||||
print(doc, target = file)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# Start ####
|
||||
|
||||
shinyApp(ui, server)
|
||||
22
SOMS2/normen/soms2_norm_a1_gesunde.csv
Normal file
22
SOMS2/normen/soms2_norm_a1_gesunde.csv
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
rohwert,dsmiv_gesamt,dsmiv_m,dsmiv_w,icd10_gesamt,icd10_m,icd10_w,sad_gesamt,sad_m,sad_w,beschwerdenindex_gesamt,beschwerdenindex_m,beschwerdenindex_w
|
||||
0,25,31,20,34,33,34,41,52,32,20,29,14
|
||||
1,43,50,37,55,60,51,61,71,54,33,38,29
|
||||
2,50,60,42,68,71,66,72,86,63,43,52,36
|
||||
3,57,67,51,76,81,73,80,86,76,51,60,44
|
||||
4,68,74,64,85,88,83,90,93,88,55,64,48
|
||||
5,81,88,76,91,93,90,96,98,95,61,74,53
|
||||
6,85,93,80,95,95,95,98,100,97,67,81,58
|
||||
7,89,95,85,96,98,95,99,100,98,72,81,66
|
||||
8,90,95,86,99,100,98,100,100,100,79,88,73
|
||||
9,93,95,92,100,100,100,100,100,100,83,88,80
|
||||
10,95,98,93,100,100,100,100,100,100,83,88,80
|
||||
11,100,100,100,100,100,100,100,100,100,84,88,81
|
||||
12,100,100,100,100,100,100,100,100,100,88,93,85
|
||||
13,100,100,100,100,100,100,100,100,100,90,95,86
|
||||
14,100,100,100,100,100,100,100,100,100,93,98,90
|
||||
15,100,100,100,100,100,100,100,100,100,95,98,93
|
||||
16,100,100,100,100,100,100,100,100,100,98,100,97
|
||||
17,100,100,100,100,100,100,100,100,100,99,100,98
|
||||
18,100,100,100,100,100,100,100,100,100,100,100,100
|
||||
19,100,100,100,100,100,100,100,100,100,100,100,100
|
||||
20,100,100,100,100,100,100,100,100,100,100,100,100
|
||||
|
42
SOMS2/normen/soms2_norm_a2_patienten.csv
Normal file
42
SOMS2/normen/soms2_norm_a2_patienten.csv
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
rohwert,dsmiv,icd10,sad,beschwerdenindex_gesamt
|
||||
0,2,4,4,1
|
||||
1,5,10,9,2
|
||||
2,8,22,17,3
|
||||
3,13,35,27,5
|
||||
4,22,46,39,7
|
||||
5,30,56,50,9
|
||||
6,37,67,63,13
|
||||
7,46,78,73,17
|
||||
8,56,84,80,23
|
||||
9,65,90,89,27
|
||||
10,72,96,94,30
|
||||
11,77,98,99,35
|
||||
12,81,99,100,41
|
||||
13,86,100,100,47
|
||||
14,90,100,100,51
|
||||
15,92,100,100,56
|
||||
16,95,100,100,63
|
||||
17,96,100,100,66
|
||||
18,98,100,100,69
|
||||
19,98,100,100,74
|
||||
20,99,100,100,77
|
||||
21,99,100,100,80
|
||||
22,99,100,100,82
|
||||
23,99,100,100,84
|
||||
24,100,100,100,87
|
||||
25,100,100,100,88
|
||||
26,100,100,100,90
|
||||
27,100,100,100,92
|
||||
28,100,100,100,94
|
||||
29,100,100,100,95
|
||||
30,100,100,100,96
|
||||
31,100,100,100,97
|
||||
32,100,100,100,98
|
||||
33,100,100,100,98
|
||||
34,100,100,100,98
|
||||
35,100,100,100,99
|
||||
36,100,100,100,99
|
||||
37,100,100,100,99
|
||||
38,100,100,100,99
|
||||
39,100,100,100,99
|
||||
40,100,100,100,100
|
||||
|
2552
SOMS2/renv.lock
Normal file
2552
SOMS2/renv.lock
Normal file
File diff suppressed because it is too large
Load diff
17
SOMS2/setup_renv.R
Normal file
17
SOMS2/setup_renv.R
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Einmalig ausfuehren, bevor die App zum ersten Mal gestartet wird.
|
||||
# Initialisiert renv und installiert alle benoetigten Pakete.
|
||||
#
|
||||
# formr wird hier installiert, weil das extern gesourcte Download-Skript
|
||||
# (get_data_soms2.R) es benoetigt - die App selbst laedt formr nicht per
|
||||
# library() und spricht nie direkt mit der formr-API.
|
||||
# DBI und RSQLite werden vom gesourcten Pseudonym-Skript benoetigt,
|
||||
# nicht direkt von der App selbst.
|
||||
|
||||
renv::init()
|
||||
|
||||
pkgs = c("shiny", "dplyr", "ggplot2", "haven", "officer", "DBI", "RSQLite", "formr")
|
||||
install.packages(pkgs)
|
||||
|
||||
renv::snapshot()
|
||||
|
||||
message("Setup abgeschlossen. App starten mit: shiny::runApp()")
|
||||
Loading…
Add table
Add a link
Reference in a new issue