DiagnostikApps/SOMS-7T/app.R
2026-09-22 18:35:43 +02:00

795 lines
30 KiB
R
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Präambel ####
PFAD_DOWNLOAD_SKRIPT = "../API/get_data_soms7t.R" # liefert: daten_soms7t
PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R" # liefert: pseudo
AKZENT_FARBE = "#8B2635"
PFAD_NORM_B1 = "normen/soms7t_norm_b1_patienten.csv"
PFAD_NORM_B2 = "normen/soms7t_norm_b2_beschwerdenanzahl_gesunde.csv"
PFAD_NORM_B3 = "normen/soms7t_norm_b3_dsmiv_gesunde.csv"
PFAD_NORM_B4 = "normen/soms7t_norm_b4_intensitaet_gesunde.csv"
PFAD_NORM_B5 = "normen/soms7t_norm_b5_nach_staerke_gesunde.csv"
SOMS7T_DISCLAIMER = paste0(
"Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ",
"keine klinische Diagnose. Die Interpretation obliegt der behandelnden Person."
)
SOMS7T_KRITERIUM_A_TEXT = paste0(
"Bei Personen, die im SOMS-7T mindestens 3 Items als »mittel ausgeprägt« ",
"bewertet bzw. einen Prozentrang > 75 haben, besteht ein hohes Risiko auf ein ",
"vorliegendes Somatisierungssyndrom."
)
SOMS7T_KRITERIUM_B_HINWEIS = paste0(
"Das Manual benennt hierzu einen Prozentrang > 75, ohne eindeutig zu spezifizieren, ",
"auf welchen der beiden Kennwerte sich dieser bezieht - beide werden daher zur ",
"Einordnung angezeigt."
)
# DSM-IV-33-Item-Liste (gleiche Itemliste wie in der SOMS-2-Schwester-App).
SOMS7T_DSMIV_ITEMS = 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)
SOMS7T_STUFEN_TEXTE = c("gar nicht", "leicht", "mittelmäßig", "stark", "sehr stark")
library(shiny)
library(dplyr)
library(ggplot2)
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_B1 = normalizePath(absPath(PFAD_NORM_B1), mustWork = FALSE)
PFAD_NORM_B2 = normalizePath(absPath(PFAD_NORM_B2), mustWork = FALSE)
PFAD_NORM_B3 = normalizePath(absPath(PFAD_NORM_B3), mustWork = FALSE)
PFAD_NORM_B4 = normalizePath(absPath(PFAD_NORM_B4), mustWork = FALSE)
PFAD_NORM_B5 = normalizePath(absPath(PFAD_NORM_B5), mustWork = FALSE)
# Helper ####
# In formr-Exporten stehen Markdown-Sternchen im Itemwortlaut und in Choice-Texten.
strip_stars = function(x) {
if (is.null(x) || length(x) == 0) return(x)
gsub("\\*\\*", "", as.character(x))
}
# Label-Text einer der fuenf bekannten Kategorien zuordnen. Laengste Kategorienamen
# zuerst pruefen, da "stark" sonst als Teilstring von "sehr stark" faelschlich zuerst
# matchen wuerde.
soms7t_kategorie_index = function(label_text) {
if (is.null(label_text) || length(label_text) == 0 || is.na(label_text[1]))
return(NA_integer_)
txt = tolower(trimws(strip_stars(label_text[1])))
reihenfolge = order(nchar(SOMS7T_STUFEN_TEXTE), decreasing = TRUE)
for (idx in reihenfolge) {
if (grepl(SOMS7T_STUFEN_TEXTE[idx], txt, fixed = TRUE)) return(idx - 1L)
}
NA_integer_
}
# Stufe (0-4) immer aus dem labels-Attribut der ORIGINAL-Spalte ableiten, nie aus dem
# formr-Rohwert 1-5 direkt, da die Kodierung je nach Setup variieren kann. Stufe = Position
# der zugeordneten Kategorie in der festen Reihenfolge "gar nicht".."sehr stark", nicht
# die numerische Position des Rohwerts (robust gegenueber abweichender formr-Kodierung).
soms7t_get_level = function(original_col, wert) {
if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(0L)
lbl = attr(original_col, "labels")
if (is.null(lbl) || length(lbl) == 0) {
# Fallback ohne labels-Attribut: 1-basierte Kodierung annehmen.
return(max(0L, min(4L, as.integer(round(as.numeric(wert[1]))) - 1L)))
}
wert_num = as.numeric(wert[1])
pos = which(as.vector(lbl) == wert_num)
if (length(pos) == 0) return(0L)
kat = soms7t_kategorie_index(names(lbl)[pos[1]])
if (is.na(kat)) return(0L)
kat
}
# label-Attribut der Spalte (Fragetext) lesen und bereinigen. Entfernt neben den
# Markdown-Sternchen auch formr-Nummerierungsartefakte am Anfang (z.B. "11\. " oder
# "11. "), die aus dem escapeten Markdown-Listenpunkt im Label-Text stammen.
soms7t_item_text = function(original_col) {
txt = attr(original_col, "label")
if (is.null(txt) || length(txt) == 0 || is.na(txt[1])) return(NA_character_)
txt = strip_stars(txt[1])
txt = sub("^\\s*\\d+\\\\?[.)]\\s*", "", txt)
trimws(txt)
}
# Label-Text (z.B. Geschlecht) aus dem labels-Attribut lesen - nie hartkodiert 1/2.
soms7t_get_label_text = function(original_col, wert) {
if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(NA_character_)
lbl = attr(original_col, "labels")
if (!is.null(lbl) && length(lbl) > 0) {
pos = which(as.vector(lbl) == as.numeric(wert[1]))
if (length(pos) > 0) return(trimws(strip_stars(names(lbl)[pos[1]])))
}
NA_character_
}
soms7t_geschlecht_spalte = function(geschlecht_text) {
if (is.na(geschlecht_text)) return("gesamt")
if (grepl("weiblich", geschlecht_text, ignore.case = TRUE)) return("frauen")
if (grepl("männlich", geschlecht_text, ignore.case = TRUE)) return("maenner")
"gesamt"
}
# B-2/B-4: Normtabellen mit rohwert_min/rohwert_max-Bereichen (mehrere Rohwerte pro Zeile
# moeglich). Rohwert oberhalb des hoechsten Tabelleneintrags -> PR 100.
pr_lookup_range = function(tabelle, rohwert, spalte,
min_col = "rohwert_min", max_col = "rohwert_max") {
if (is.na(rohwert)) return(NA_real_)
if (rohwert > max(tabelle[[max_col]], na.rm = TRUE)) return(100)
zeile = tabelle[tabelle[[min_col]] <= rohwert & tabelle[[max_col]] >= rohwert, ]
if (nrow(zeile) == 0) return(NA_real_)
as.numeric(zeile[[spalte]][1])
}
# B-1/B-3: Normtabellen mit exaktem rohwert pro Zeile.
pr_lookup_exact = function(tabelle, rohwert, spalte, rohwert_col = "rohwert") {
if (is.na(rohwert)) return(NA_real_)
if (rohwert > max(tabelle[[rohwert_col]], na.rm = TRUE)) return(100)
zeile = tabelle[tabelle[[rohwert_col]] == rohwert, ]
if (nrow(zeile) == 0) return(NA_real_)
as.numeric(zeile[[spalte]][1])
}
# B-5: PR der Beschwerdenanzahl nach Symptomstaerke-Schwelle. Leere Zellen bedeuten
# "bei dieser Schwelle nicht erreichbar" -> NA, kein erzwungener Wert.
pr_lookup_b5 = function(tabelle, beschwerdenanzahl, spalte = "staerke_2_4") {
if (is.na(beschwerdenanzahl)) return(NA_real_)
if (beschwerdenanzahl > max(tabelle[["beschwerdenanzahl"]], na.rm = TRUE)) return(100)
zeile = tabelle[tabelle[["beschwerdenanzahl"]] == beschwerdenanzahl, ]
if (nrow(zeile) == 0) return(NA_real_)
wert = suppressWarnings(as.numeric(zeile[[spalte]][1]))
if (is.na(wert)) return(NA_real_)
wert
}
# Dichotomisierung: Stufe 0-1 -> 0 Punkte, Stufe 2-4 -> 1 Punkt.
soms7t_berechne_beschwerdenanzahl = function(stufen_vektor) {
sum(stufen_vektor >= 2, na.rm = TRUE)
}
# Keine Dichotomisierung: Rohsumme der Stufenwerte (0-4).
soms7t_berechne_intensitaet = function(stufen_vektor) {
sum(stufen_vektor, na.rm = TRUE)
}
soms7t_kriterium_a = function(beschwerdenanzahl) {
isTRUE(beschwerdenanzahl >= 3)
}
soms7t_format_pr = function(pr) {
if (is.null(pr) || length(pr) == 0 || is.na(pr)) return("—")
paste0("PR ", format(round(pr, 1), nsmall = 1))
}
# Vier Stufen-Badge-Klassen (gruen -> dunkelrot), 1:1 aus der BDI-II-App uebernommen.
# Rohwert wird per min(stufe, 3L) gedeckelt - Stufe 3 "stark" und Stufe 4 "sehr stark"
# teilen sich damit die dunkelste Badge-Farbe (nur 4 Klassen vorgesehen, siehe Abschnitt 5).
soms7t_badge_klasse = function(stufe) {
paste0("stufe-badge-", min(max(stufe, 0L), 3L))
}
SOMS7T_BADGE_WORD_FARBEN = list(
"0" = list(bg = "#C8E6C9", text = "#1B5E20"),
"1" = list(bg = "#FFCDD2", text = "#B71C1C"),
"2" = list(bg = "#EF9A9A", text = "#7B0000"),
"3" = list(bg = "#B71C1C", text = "#FFFFFF")
)
# Datenaufbereitung ####
.soms7t_norm_pfade = c(
"B-1 (Patientennorm)" = PFAD_NORM_B1,
"B-2 (Beschwerdenanzahl, Gesunde)" = PFAD_NORM_B2,
"B-3 (DSM-IV, Gesunde)" = PFAD_NORM_B3,
"B-4 (Intensitaetsindex, Gesunde)" = PFAD_NORM_B4,
"B-5 (nach Symptomstaerke, Gesunde)" = PFAD_NORM_B5
)
.soms7t_fehlende_normen = names(.soms7t_norm_pfade)[!file.exists(.soms7t_norm_pfade)]
if (length(.soms7t_fehlende_normen) > 0) {
stop(
"SOMS-7T: Folgende Normtabellen-CSVs fehlen im Ordner 'normen/':\n",
paste0(" - ", .soms7t_fehlende_normen, ": ",
.soms7t_norm_pfade[.soms7t_fehlende_normen], collapse = "\n"),
"\nBitte die fehlenden Dateien ablegen und die App neu starten."
)
}
norm_b1 = read.csv(PFAD_NORM_B1, stringsAsFactors = FALSE)
norm_b2 = read.csv(PFAD_NORM_B2, stringsAsFactors = FALSE)
norm_b3 = read.csv(PFAD_NORM_B3, stringsAsFactors = FALSE)
norm_b4 = read.csv(PFAD_NORM_B4, stringsAsFactors = FALSE)
norm_b5 = read.csv(PFAD_NORM_B5, stringsAsFactors = FALSE)
# 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; }
.kennwert-block {
display: flex; gap: 28px; flex-wrap: wrap; align-items: flex-start;
padding: 10px 0;
}
.kennwert-zahl { font-size: 2.0rem; font-weight: 800; color: #8B2635; }
.kennwert-label { color: #555; font-size: 0.88em; margin-top: 2px; }
.kennwert-pr { font-size: 0.95em; color: #333; margin-top: 4px; }
.kennwert-pr-patient { font-size: 0.85em; color: #777; margin-top: 2px; font-style: italic; }
.kriterium-box {
border-radius: 6px; padding: 14px 18px; margin: 10px 0;
border-left: 5px solid;
}
.kriterium-erfuellt { background: #FFEBEE; border-color: #EF9A9A; color: #B71C1C; }
.kriterium-nicht { background: #F5F5F5; border-color: #9E9E9E; color: #424242; }
.kriterium-titel { font-weight: 700; font-size: 1.0rem; margin-bottom: 6px; }
.kriterium-text { font-size: 0.92em; line-height: 1.5; }
.kriterium-b-werte {
display: flex; gap: 24px; margin-top: 8px; flex-wrap: wrap; font-size: 0.92em;
}
.kriterium-b-hinweis {
font-size: 0.82em; color: #777; font-style: italic; margin-top: 8px;
}
.item-zeile {
display: flex; align-items: flex-start; gap: 10px;
padding: 7px 0; border-bottom: 1px solid #F0F0F0;
}
.item-nr { font-weight: 600; color: #8B2635; min-width: 26px; flex-shrink: 0; }
.item-text { flex: 1; color: #333; font-size: 0.92em; }
.stufe-badge {
border-radius: 4px; padding: 2px 9px; font-weight: 700;
font-size: 0.82em; white-space: nowrap; display: inline-block; flex-shrink: 0;
}
.stufe-badge-0 { background-color: #C8E6C9; color: #1B5E20; }
.stufe-badge-1 { background-color: #FFCDD2; color: #B71C1C; }
.stufe-badge-2 { background-color: #EF9A9A; color: #7B0000; }
.stufe-badge-3 { background-color: #B71C1C; color: white; }
.disclaimer-zeile {
font-size: 0.82em; color: #777; font-style: italic;
margin-top: 10px; border-top: 1px solid rgba(0,0,0,.1); padding-top: 8px;
}
"
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-7T Screening für somatoforme Störungen (7-Tage-Version)"),
tags$p("Beschwerdenanzahl & Intensitätsindex mit Normvergleich")
),
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)")
)
),
uiOutput("fehler_ui"),
uiOutput("warnung_ui"),
uiOutput("ergebnis_ui")
)
)
# Word-Export ####
erstelle_soms7t_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_disclaimer = fp_text(font.size = 9, italic = TRUE, color = "#777777")
doc = body_add_fpar(doc, fpar(ftext("SOMS-7T — Auswertung", fp_titel)))
doc = body_add_fpar(doc, fpar(
ftext("Chiffre: ", fp_label),
ftext(erg$chiffre, fp_normal),
ftext(" Datum: ", 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$info_mehrere)) {
doc = body_add_fpar(doc, fpar(
ftext(erg$info_mehrere, fp_text(font.size = 10, italic = TRUE, color = "#555555"))
))
}
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("Beschwerdenanzahl", fp_abschnitt)))
doc = body_add_fpar(doc, fpar(
ftext("Rohwert (alle 53 Items, Stufe ≥2): ", fp_label),
ftext(paste0(erg$beschwerden_gesamt, " ",
soms7t_format_pr(erg$pr_beschwerden_gesund),
" (Gesunden-Norm, geschlechtsgetrennt)"), fp_normal)
))
doc = body_add_fpar(doc, fpar(
ftext("Vergleichswert: psychosomatische Patienten (Veraenderungsmessung): ",
fp_text(font.size = 10, italic = TRUE, color = "#555555")),
ftext(soms7t_format_pr(erg$pr_beschwerden_patient),
fp_text(font.size = 10, italic = TRUE, color = "#555555"))
))
doc = body_add_fpar(doc, fpar(
ftext("Rohwert DSM-IV-Liste (33 Items, Stufe ≥2): ", fp_label),
ftext(paste0(erg$beschwerden_dsmiv, " ", soms7t_format_pr(erg$pr_dsmiv)), fp_normal)
))
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("Intensitätsindex", fp_abschnitt)))
doc = body_add_fpar(doc, fpar(
ftext("Rohwert (Summe aller 53 Item-Stufen, 0-212): ", fp_label),
ftext(paste0(erg$intensitaet, " ",
soms7t_format_pr(erg$pr_intensitaet_gesund),
" (Gesunden-Norm, geschlechtsgetrennt)"), fp_normal)
))
doc = body_add_fpar(doc, fpar(
ftext("Vergleichswert: psychosomatische Patienten (Veraenderungsmessung): ",
fp_text(font.size = 10, italic = TRUE, color = "#555555")),
ftext(soms7t_format_pr(erg$pr_intensitaet_patient),
fp_text(font.size = 10, italic = TRUE, color = "#555555"))
))
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("Screening-Kriterium", fp_abschnitt)))
if (erg$kriterium_a) {
doc = body_add_fpar(doc, fpar(ftext(
paste0("Screening-Kriterium A erfuellt (≥3 Items mittelmaessig+): ",
erg$beschwerden_gesamt, " Items."),
fp_text(bold = TRUE, font.size = 11, color = "#B71C1C")
)))
doc = body_add_fpar(doc, fpar(ftext(SOMS7T_KRITERIUM_A_TEXT,
fp_text(font.size = 10, color = "#B71C1C"))))
} else {
doc = body_add_fpar(doc, fpar(ftext(
paste0("Screening-Kriterium A nicht erfuellt (", erg$beschwerden_gesamt,
" von 3 Items mittelmaessig+)."),
fp_text(bold = TRUE, font.size = 11, color = "#424242")
)))
}
doc = body_add_fpar(doc, fpar(
ftext("Kriterium B (Zusatzinformation) - PR Beschwerdenanzahl (Schwelle 2-4): ",
fp_label),
ftext(soms7t_format_pr(erg$pr_staerke_2_4), fp_normal),
ftext(" PR Intensitätsindex: ", fp_label),
ftext(soms7t_format_pr(erg$pr_intensitaet_gesund), fp_normal)
))
doc = body_add_fpar(doc, fpar(ftext(SOMS7T_KRITERIUM_B_HINWEIS,
fp_text(font.size = 9, italic = TRUE, color = "#777777"))))
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("Items mit Stufe ≥ mittelmäßig", fp_abschnitt)))
if (nrow(erg$items_liste) == 0) {
doc = body_add_fpar(doc, fpar(ftext("Keine Items mit Stufe ≥2 vorhanden.", fp_normal)))
} else {
for (r in seq_len(nrow(erg$items_liste))) {
zeile = erg$items_liste[r, ]
badge_key = as.character(min(max(zeile$stufe, 0L), 3L))
farben = SOMS7T_BADGE_WORD_FARBEN[[badge_key]]
fp_badge = fp_text(color = farben$text, bold = TRUE,
shading.color = farben$bg, font.size = 10)
doc = body_add_fpar(doc, fpar(
ftext(paste0(zeile$nummer, ". ", zeile$text, " "), fp_normal),
ftext(paste0(" ", SOMS7T_STUFEN_TEXTE[zeile$stufe + 1L], " "), fp_badge)
))
}
}
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext(SOMS7T_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)))
}
})
ergebnis_r = eventReactive(input$btn_suchen, {
chiffre = toupper(trimws(input$chiffre))
if (nchar(trimws(input$pseudonym)) == 0 && nchar(chiffre) == 0)
return(list(error = "Bitte eine Patientenchiffre eingeben."))
if (!(nchar(trimws(input$pseudonym)) > 0 || grepl("^[A-Z][0-9]{6}$", chiffre)))
return(list(error = paste0(
"Ungültige Chiffre. Erwartet: ein Großbuchstabe + 6 Ziffern (z.B. P000123).")))
if (!file.exists(PFAD_DOWNLOAD_SKRIPT))
return(list(error = paste0(
"Download-Skript nicht gefunden:\n", PFAD_DOWNLOAD_SKRIPT)))
if (!file.exists(PFAD_PSEUDONYM_SKRIPT))
return(list(error = paste0(
"Pseudonym-Skript nicht gefunden:\n", PFAD_PSEUDONYM_SKRIPT)))
res_dl = tryCatch(
{ source(PFAD_DOWNLOAD_SKRIPT, local = FALSE); list(ok = TRUE) },
error = function(e) list(ok = FALSE, msg = e$message)
)
if (!res_dl$ok)
return(list(error = paste0("Fehler im Download-Skript: ", res_dl$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
})
alter_wd = getwd()
wd_ziel = if (!is.null(db_ordner)) db_ordner else
dirname(normalizePath(PFAD_PSEUDONYM_SKRIPT, mustWork = FALSE))
setwd(wd_ziel)
on.exit(setwd(alter_wd), add = TRUE)
res_ps = tryCatch(
{ source(PFAD_PSEUDONYM_SKRIPT, local = FALSE)
if (nchar(trimws(input$pseudonym)) > 0) {
.pw_wert = trimws(input$pseudonym)
.pw_tab = get("pseudo", envir = .GlobalEnv)
.pw_treffer = .pw_tab[.pw_tab$pseudonym == .pw_wert, ]
if (nrow(.pw_treffer) > 0) chiffre = toupper(trimws(.pw_treffer$chiffre[1]))
}; list(ok = TRUE) },
error = function(e) list(ok = FALSE, msg = e$message)
)
if (!res_ps$ok)
return(list(error = paste0("Fehler im Pseudonym-Skript: ", res_ps$msg)))
if (!exists("daten_soms7t", envir = .GlobalEnv))
return(list(error = paste0(
"Objekt 'daten_soms7t' nach dem Sourcen nicht gefunden. ",
"Bitte Download-Skript prüfen.")))
if (!exists("pseudo", envir = .GlobalEnv))
return(list(error = paste0(
"Objekt 'pseudo' nach dem Sourcen nicht gefunden. ",
"Bitte Pseudonym-Skript prüfen.")))
daten = get("daten_soms7t", envir = .GlobalEnv)
pseudo_df = get("pseudo", envir = .GlobalEnv)
treffer_ps = pseudo_df[pseudo_df$chiffre == chiffre, ]
if (nrow(treffer_ps) == 0)
return(list(error = paste0(
"Chiffre '", chiffre, "' wurde in der Pseudonym-Datenbank nicht gefunden.")))
alle_session_ids = unique(treffer_ps$pseudonym)
if (nchar(trimws(input$pseudonym)) > 0) alle_session_ids = trimws(input$pseudonym)
treffer_dat = daten[daten$session %in% alle_session_ids, ]
if (nrow(treffer_dat) == 0)
return(list(error = paste0(
"Kein SOMS-7T-Datensatz für Chiffre '", chiffre, "' gefunden. ",
"(", length(alle_session_ids), " Pseudonym(e) geprüft)")))
info_mehrere = 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"
)
info_mehrere = paste0(
"Mehrere Ausfüllungen gefunden (", n, " Einträge). ",
"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 = soms7t_get_label_text(daten[["soms7t_geschlecht"]],
zeile[["soms7t_geschlecht"]])
geschlecht_spalte = soms7t_geschlecht_spalte(geschlecht_text)
stufen = integer(53)
item_texte = character(53)
for (i in seq_len(53)) {
var = paste0("soms7t_", sprintf("%02d", i))
stufen[i] = soms7t_get_level(daten[[var]], zeile[[var]])
item_texte[i] = soms7t_item_text(daten[[var]])
if (is.na(item_texte[i])) item_texte[i] = paste0("Item ", i)
}
beschwerden_gesamt = soms7t_berechne_beschwerdenanzahl(stufen)
beschwerden_dsmiv = soms7t_berechne_beschwerdenanzahl(stufen[SOMS7T_DSMIV_ITEMS])
intensitaet = soms7t_berechne_intensitaet(stufen)
pr_beschwerden_gesund = pr_lookup_range(norm_b2, beschwerden_gesamt, geschlecht_spalte)
pr_beschwerden_patient = pr_lookup_exact(norm_b1, beschwerden_gesamt, "pr_beschwerdenanzahl")
pr_dsmiv = pr_lookup_exact(norm_b3, beschwerden_dsmiv, geschlecht_spalte)
pr_intensitaet_gesund = pr_lookup_range(norm_b4, intensitaet, geschlecht_spalte)
pr_intensitaet_patient = pr_lookup_exact(norm_b1, intensitaet, "pr_intensitaetsindex")
pr_staerke_2_4 = pr_lookup_b5(norm_b5, beschwerden_gesamt, "staerke_2_4")
kriterium_a = soms7t_kriterium_a(beschwerden_gesamt)
items_idx = which(stufen >= 2)
items_liste = data.frame(
nummer = items_idx,
text = item_texte[items_idx],
stufe = stufen[items_idx],
stringsAsFactors = FALSE
)
list(
chiffre = chiffre,
datum_str = datum_str,
info_mehrere = info_mehrere,
geschlecht_text = geschlecht_text,
beschwerden_gesamt = beschwerden_gesamt,
beschwerden_dsmiv = beschwerden_dsmiv,
intensitaet = intensitaet,
pr_beschwerden_gesund = pr_beschwerden_gesund,
pr_beschwerden_patient = pr_beschwerden_patient,
pr_dsmiv = pr_dsmiv,
pr_intensitaet_gesund = pr_intensitaet_gesund,
pr_intensitaet_patient = pr_intensitaet_patient,
pr_staerke_2_4 = pr_staerke_2_4,
kriterium_a = kriterium_a,
items_liste = items_liste,
error = NULL
)
})
output$fehler_ui = renderUI({
req(input$btn_suchen)
d = ergebnis_r()
if (!is.null(d$error)) div(class = "alert-fehler", d$error)
})
output$warnung_ui = renderUI({
req(input$btn_suchen)
d = ergebnis_r()
if (!is.null(d$error) || is.null(d$info_mehrere)) return(NULL)
div(class = "alert-warnung", d$info_mehrere)
})
output$ergebnis_ui = renderUI({
req(input$btn_suchen)
d = ergebnis_r()
if (!is.null(d$error)) return(NULL)
items_ui = if (nrow(d$items_liste) == 0) {
div(style = "color:#777; font-style:italic;", "Keine Items mit Stufe ≥2 vorhanden.")
} else {
lapply(seq_len(nrow(d$items_liste)), function(r) {
zeile = d$items_liste[r, ]
div(class = "item-zeile",
div(class = "item-nr", paste0(zeile$nummer, ".")),
div(class = "item-text", zeile$text),
span(class = paste0("stufe-badge ", soms7t_badge_klasse(zeile$stufe)),
SOMS7T_STUFEN_TEXTE[zeile$stufe + 1L])
)
})
}
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "SOMS-7T Auswertung"),
div(class = "meta-block",
tags$strong("Chiffre: "), d$chiffre,
tags$span(" | ", style = "color:#ccc;"),
tags$strong("Ausfülldatum: "), d$datum_str,
tags$span(" | ", style = "color:#ccc;"),
tags$strong("Geschlecht: "), if (is.na(d$geschlecht_text)) "k. A." else d$geschlecht_text
),
tags$hr(),
tags$h5("Beschwerdenanzahl"),
div(class = "kennwert-block",
div(
div(class = "kennwert-zahl", d$beschwerden_gesamt),
div(class = "kennwert-label", "Rohwert (alle 53 Items, Stufe ≥2)"),
div(class = "kennwert-pr", soms7t_format_pr(d$pr_beschwerden_gesund),
" (Gesunden-Norm, geschlechtsgetrennt)"),
div(class = "kennwert-pr-patient",
"Vergleichswert: psychosomatische Patienten (Veränderungsmessung): ",
soms7t_format_pr(d$pr_beschwerden_patient))
),
div(
div(class = "kennwert-zahl", d$beschwerden_dsmiv),
div(class = "kennwert-label", "Rohwert DSM-IV-Liste (33 Items, Stufe ≥2)"),
div(class = "kennwert-pr", soms7t_format_pr(d$pr_dsmiv))
)
),
tags$hr(),
tags$h5("Intensitätsindex"),
div(class = "kennwert-block",
div(
div(class = "kennwert-zahl", d$intensitaet),
div(class = "kennwert-label", "Rohwert (Summe aller 53 Item-Stufen, 0-212)"),
div(class = "kennwert-pr", soms7t_format_pr(d$pr_intensitaet_gesund),
" (Gesunden-Norm, geschlechtsgetrennt)"),
div(class = "kennwert-pr-patient",
"Vergleichswert: psychosomatische Patienten (Veränderungsmessung): ",
soms7t_format_pr(d$pr_intensitaet_patient))
)
),
tags$hr(),
tags$h5("Screening-Kriterium"),
div(class = paste0("kriterium-box ", if (d$kriterium_a) "kriterium-erfuellt" else "kriterium-nicht"),
div(class = "kriterium-titel",
if (d$kriterium_a)
paste0("Screening-Kriterium A erfüllt (≥3 Items mittelmäßig+): ",
d$beschwerden_gesamt, " Items")
else
paste0("Screening-Kriterium A nicht erfüllt (", d$beschwerden_gesamt,
" von 3 Items mittelmäßig+)")
),
if (d$kriterium_a) div(class = "kriterium-text", SOMS7T_KRITERIUM_A_TEXT),
div(class = "kriterium-b-werte",
div(tags$strong("Kriterium B PR Beschwerdenanzahl (Schwelle 2-4): "),
soms7t_format_pr(d$pr_staerke_2_4)),
div(tags$strong("PR Intensitätsindex: "), soms7t_format_pr(d$pr_intensitaet_gesund))
),
div(class = "kriterium-b-hinweis", SOMS7T_KRITERIUM_B_HINWEIS)
),
tags$hr(),
tags$h5("Items mit Stufe ≥ mittelmäßig"),
div(items_ui),
div(class = "disclaimer-zeile", SOMS7T_DISCLAIMER)
)
})
output$download_word = downloadHandler(
filename = function() {
d = tryCatch(ergebnis_r(), error = function(e) NULL)
chiffre_esc = if (is.list(d) && is.null(d$error) && nchar(d$chiffre) > 0)
gsub("[^A-Za-z0-9_-]", "_", d$chiffre) else "export"
datum = if (is.list(d) && is.null(d$error) && !is.null(d$datum_str))
tryCatch(
format(as.Date(d$datum_str, "%d.%m.%Y"), "%Y%m%d"),
error = function(e) format(Sys.Date(), "%Y%m%d")
)
else
format(Sys.Date(), "%Y%m%d")
paste0("SOMS7T_", chiffre_esc, "_", datum, ".docx")
},
content = function(file) {
d = tryCatch(ergebnis_r(), error = function(e) NULL)
daten_ok = is.list(d) && is.null(d$error)
if (!daten_ok) {
doc = read_docx()
doc = body_add_par(doc,
"Kein Datensatz geladen. Bitte zuerst Chiffre eingeben und 'Auswerten' klicken.",
style = "Normal")
print(doc, target = file)
return()
}
doc = tryCatch(
erstelle_soms7t_docx(d),
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)