795 lines
30 KiB
R
795 lines
30 KiB
R
# 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)
|