DiagnostikApps/CSAS-E/app.R
2026-09-22 18:35:43 +02:00

1251 lines
48 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 ####
AKZENT_FARBE = "#8B2635"
PFAD_DOWNLOAD_SKRIPT = "../API/get_data_csas_e.R"
PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R"
PFAD_NORMTABELLEN = "normtabellen"
CSAS_DISCLAIMER = paste0(
"Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ",
"keine klinische Diagnose. Die Interpretation obliegt der behandelnden Person. ",
"Der Cutoff von 5 erfuellten DSM-5-Kriterien ist laut Testmanual eine pragmatische, ",
"bewusst konservative Konvention und keine validierte diagnostische Schwelle."
)
CSAS_KONVENTION_HINWEIS = paste0(
"Hinweis: Die APA-Konvention '5 von 9 Kriterien' ist laut Testmanual eine ",
"pragmatische, bewusst konservative Konvention, keine harte Diagnoseschwelle. ",
"Die CSAS arbeitet grundsaetzlich nur verdachtsdiagnostisch."
)
CSAS_NORMWERT_ALLGEMEIN_HINWEIS = paste0(
"Prozentrang-/Stanine-Einordnung nach Alter und Geschlecht ist fuer CSAS-E im ",
"Testmanual vorgesehen. Verfuegbar sind digitalisierte Normtabellen fuer die ",
"Altersgruppen 16-30 und 31-49 Jahre (je getrennt nach Geschlecht und als ",
"Gesamtstichprobe)."
)
CSAS_NORMTABELLEN_SPALTEN = c("stanine", "prozentrang", "spielzeit_min", "spielzeit_max",
"summenscore_min", "summenscore_max")
CSAS_ITEM_CHOICE_TEXTE = c("stimmt nicht", "stimmt kaum", "stimmt eher", "stimmt genau")
CSAS_GESCHLECHT_CHOICE_TEXTE = c("männlich", "weiblich")
CSAS_GERAETE_SPALTEN = c(
pc = "csas_e_geraet_pc",
konsole = "csas_e_geraet_konsole",
tragbar = "csas_e_geraet_tragbar",
handy = "csas_e_geraet_handy"
)
CSAS_GERAETE_ANZEIGE = c(
pc = "PC",
konsole = "Spielkonsole",
tragbar = "Tragbares Geraet (Handheld/Tablet)",
handy = "Handy/Smartphone"
)
CSAS_DSM_KRITERIEN = list(
list(nr = 1, name = "Gedankliche Vereinnahmung", items = c(1, 8)),
list(nr = 2, name = "Entzugserscheinungen", items = c(5, 7)),
list(nr = 3, name = "Toleranzentwicklung", items = c(2, 4)),
list(nr = 4, name = "Kontrollverlust", items = c(3, 10)),
list(nr = 5, name = "Verhaltensbezogene Einengung", items = c(11, 15)),
list(nr = 6, name = "Fortsetzung trotz psychosozialer Probleme", items = c(6, 14)),
list(nr = 7, name = "Luegen/Verheimlichen", items = c(13, 17)),
list(nr = 8, name = "Dysfunktionale Gefuehlsregulation", items = c(9, 12)),
list(nr = 9, name = "Gefaehrdung/Verluste", items = c(16, 18))
)
CSAS_BADGE_FARBEN = c(
"0" = "#4CAF50",
"1" = "#F48FB1",
"2" = "#EF5350",
"3" = "#B71C1C"
)
CSAS_BADGE_TEXT_FARBEN = c(
"0" = "white",
"1" = "#333333",
"2" = "white",
"3" = "white"
)
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_NORMTABELLEN = normalizePath(absPath(PFAD_NORMTABELLEN), mustWork = FALSE)
# Helper ####
# Zentrale Markdown-Bereinigung. Muss auf Choice-Texte, Item-Fragetexte und
# Freitexte konsequent angewendet werden, bevor sie verglichen oder angezeigt werden.
bereinige_markdown = function(x) {
if (is.null(x) || length(x) == 0 || is.na(x[1])) return(NA_character_)
x = as.character(x[1])
x = gsub("\\*\\*", "", x)
x = gsub("(?<!\\\\)\\*", "", x, perl = TRUE)
x = gsub("\\\\\\.", ".", x)
x = trimws(x)
x
}
# Itemnummer und Fragetext trennen. Rohlabel-Format: "1\\. Ich beschaeftige mich..."
csas_e_parse_item_label = function(spalte_original) {
roh = attr(spalte_original, "label")
if (is.null(roh) || length(roh) == 0 || is.na(roh[1]) || nchar(trimws(as.character(roh[1]))) == 0) {
return(list(nr = NA_integer_, text = NA_character_))
}
roh = as.character(roh)
treffer = regmatches(roh, regexec("^(\\d+)\\\\\\.\\s*(.*)$", roh))
if (length(treffer) > 0 && length(treffer[[1]]) == 3 && nchar(treffer[[1]][1]) > 0) {
return(list(nr = as.integer(treffer[[1]][2]), text = bereinige_markdown(treffer[[1]][3])))
}
list(nr = NA_integer_, text = bereinige_markdown(roh))
}
# Extrahiert aus einer einzelnen Zelle den bereinigten Rohtext bzw. einen rein
# numerischen 1-basierten Index, unabhaengig vom Kodierungsformat. Liefert
# ok = FALSE nur, wenn wirklich kein Format erkannt werden konnte.
csas_e_ermittle_choice_roh = function(wert) {
if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) {
return(list(ok = TRUE, fehlt = TRUE, text = NA_character_, index_num = NA_integer_))
}
wert = wert[1]
if (haven::is.labelled(wert)) {
lbl = attr(wert, "labels")
if (!is.null(lbl) && length(lbl) > 0) {
pos = which(as.vector(lbl) == as.numeric(wert))
if (length(pos) > 0) {
return(list(ok = TRUE, fehlt = FALSE,
text = bereinige_markdown(names(lbl)[pos[1]]),
index_num = NA_integer_))
}
}
return(list(ok = FALSE,
fehler = "labelled-Spalte ohne passenden Label-Eintrag fuer den vorliegenden Rohwert"))
}
if (is.character(wert) || is.factor(wert)) {
text = bereinige_markdown(as.character(wert))
if (is.na(text) || nchar(text) == 0) {
return(list(ok = TRUE, fehlt = TRUE, text = NA_character_, index_num = NA_integer_))
}
return(list(ok = TRUE, fehlt = FALSE, text = text, index_num = NA_integer_))
}
if (is.numeric(wert)) {
idx = suppressWarnings(as.integer(wert))
if (!is.na(idx)) {
return(list(ok = TRUE, fehlt = FALSE, text = NA_character_, index_num = idx))
}
}
list(ok = FALSE, fehler = "weder labelled noch Text noch numerischer Index")
}
# Kernfunktion lt. Vorgabe: liefert den 0-basierten recodierten Wert (Position in
# choice_texte minus 1) fuer ein Choice-Item. Rät bei unbekanntem Format NICHTS,
# sondern liefert ok = FALSE mit klarer Fehlermeldung.
hole_item_wert = function(spalte, choice_texte, spaltenname = "") {
roh = csas_e_ermittle_choice_roh(spalte)
choice_bereinigt = vapply(choice_texte, bereinige_markdown, character(1), USE.NAMES = FALSE)
if (!roh$ok) {
return(list(ok = FALSE, fehler = paste0(
"Unbekanntes Kodierungsformat in Spalte '", spaltenname, "', bitte manuell pruefen. (",
roh$fehler, ")"
)))
}
if (isTRUE(roh$fehlt)) {
return(list(ok = TRUE, fehlt = TRUE, index = NA_integer_, wert = NA_integer_, text = NA_character_))
}
if (!is.na(roh$text)) {
idx = match(tolower(roh$text), tolower(choice_bereinigt))
if (!is.na(idx)) {
return(list(ok = TRUE, fehlt = FALSE, index = idx, wert = idx - 1L, text = choice_bereinigt[idx]))
}
return(list(ok = FALSE, fehler = paste0(
"Unbekanntes Kodierungsformat in Spalte '", spaltenname, "': Text '", roh$text,
"' passt zu keinem der erwarteten Choice-Texte (", paste(choice_bereinigt, collapse = " / "),
"). Bitte manuell pruefen."
)))
}
if (!is.na(roh$index_num) && roh$index_num >= 1L && roh$index_num <= length(choice_texte)) {
return(list(ok = TRUE, fehlt = FALSE, index = roh$index_num, wert = roh$index_num - 1L,
text = choice_bereinigt[roh$index_num]))
}
list(ok = FALSE, fehler = paste0(
"Unbekanntes Kodierungsformat in Spalte '", spaltenname, "', bitte manuell pruefen."
))
}
# Fuer die Geraete-Matrix-Items ist nur relevant, ob Choice 1 ("nie") vorliegt.
# Die vollstaendige 7-stufige Choice-Text-Liste ist nicht dokumentiert - daher
# kein Abgleich gegen eine vollstaendige Vokabelliste (das wuerde bei jeder
# Nicht-"nie"-Antwort faelschlich einen Formatfehler ausloesen), sondern direkter
# Vergleich des ermittelten Textes mit "nie" bzw. Pruefung auf Index 1.
pruefe_geraet_nie = function(spalte, spaltenname = "") {
roh = csas_e_ermittle_choice_roh(spalte)
if (!roh$ok) {
return(list(ok = FALSE, fehler = paste0(
"Unbekanntes Kodierungsformat in Spalte '", spaltenname, "', bitte manuell pruefen. (",
roh$fehler, ")"
)))
}
if (isTRUE(roh$fehlt)) {
return(list(ok = TRUE, fehlt = TRUE, nie = NA, text = NA_character_))
}
if (!is.na(roh$text)) {
return(list(ok = TRUE, fehlt = FALSE, nie = identical(tolower(roh$text), "nie"), text = roh$text))
}
if (!is.na(roh$index_num)) {
anzeige_text = if (roh$index_num == 1L) "nie" else paste0("Stufe ", roh$index_num)
return(list(ok = TRUE, fehlt = FALSE, nie = (roh$index_num == 1L), text = anzeige_text))
}
list(ok = FALSE, fehler = paste0(
"Unbekanntes Kodierungsformat in Spalte '", spaltenname, "', bitte manuell pruefen."
))
}
# HH:MM-Text robust parsen, kein stiller Fallback bei unerwartetem Format.
# formr liefert dieses Feld faktisch als HH:MM:SS (abweichend von der urspruenglich
# angenommenen Doku HH:MM) - Sekunden werden akzeptiert und schlicht ignoriert.
csas_e_parse_hhmm = function(x, feldname) {
if (is.null(x) || length(x) == 0 || is.na(x[1]) || trimws(as.character(x[1])) == "") {
return(list(ok = FALSE, minuten = NA_real_,
fehler = paste0("Spielzeit-Feld '", feldname, "' ist leer.")))
}
x = trimws(as.character(x[1]))
if (!grepl("^[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?$", x)) {
return(list(ok = FALSE, minuten = NA_real_, fehler = paste0(
"Spielzeit-Feld '", feldname, "' hat unerwartetes Format ('", x, "', erwartet HH:MM oder HH:MM:SS)."
)))
}
stunden = as.numeric(sub(":.*", "", x))
minuten = as.numeric(sub("^[0-9]{1,2}:([0-9]{2}).*$", "\\1", x))
list(ok = TRUE, minuten = stunden * 60 + minuten, fehler = NULL)
}
csas_e_minuten_zu_text = function(minuten) {
if (is.na(minuten)) return("k. A.")
h = floor(minuten / 60)
m = round(minuten %% 60)
sprintf("%d:%02d Std.", h, m)
}
csas_e_erstellungsdatum_parsen = function(x) {
if (is.null(x) || length(x) == 0 || is.na(x[1])) {
return(list(ok = FALSE, fehler = "Zeitstempel 'created' ist leer oder fehlt fuer diesen Datensatz."))
}
dt = tryCatch(as.POSIXct(x[1]), error = function(e) NA)
if (length(dt) == 0 || is.na(dt)) {
return(list(ok = FALSE, fehler = paste0(
"Zeitstempel 'created' konnte nicht geparst werden: '", x[1], "'."
)))
}
list(ok = TRUE, dt = dt)
}
csas_e_einordnung = function(anzahl) {
if (anzahl <= 1) {
return(list(label = "unauffaellig", farbe = "#2E7D32", bg = "#E8F5E9",
border = "#A5D6A7", key = "unauffaellig"))
}
if (anzahl <= 4) {
return(list(label = "riskant / moegliche Gefaehrdung", farbe = "#E65100", bg = "#FFF3E0",
border = "#FFCC80", key = "riskant"))
}
list(label = "pathologisch / Verdacht auf Internet Gaming Disorder (IGD)",
farbe = "#B71C1C", bg = "#FFEBEE", border = "#EF9A9A", key = "pathologisch")
}
make_kriterien_balken = function(anzahl) {
zone_df = data.frame(
xmin = c(-0.5, 1.5, 4.5),
xmax = c(1.5, 4.5, 9.5),
fill = c("#E8F5E9", "#FFF3E0", "#FFEBEE"),
stringsAsFactors = FALSE
)
ggplot() +
geom_rect(data = zone_df,
aes(xmin = xmin, xmax = xmax, ymin = 0, ymax = 1, fill = fill),
color = NA) +
scale_fill_identity() +
geom_rect(aes(xmin = -0.5, xmax = 9.5, ymin = 0, ymax = 1),
fill = NA, color = "#9E9E9E", linewidth = 0.6) +
geom_vline(xintercept = c(1.5, 4.5), color = "#9E9E9E",
linetype = "dashed", linewidth = 0.5) +
geom_segment(aes(x = anzahl, xend = anzahl, y = -0.25, yend = 1.25),
color = AKZENT_FARBE, linewidth = 2.5) +
geom_label(aes(x = anzahl, y = 1.6, label = paste0(anzahl, " / 9")),
fill = AKZENT_FARBE, color = "white", fontface = "bold",
linewidth = 0, size = 4) +
annotate("text", x = 0.5, y = -0.55, label = "0-1 unauffaellig", color = "#2E7D32", size = 3.2) +
annotate("text", x = 3, y = -0.55, label = "2-4 riskant", color = "#E65100", size = 3.2) +
annotate("text", x = 7, y = -0.55, label = "5-9 pathologisch", color = "#B71C1C", size = 3.2) +
scale_x_continuous(limits = c(-2, 11), breaks = 0:9) +
scale_y_continuous(limits = c(-0.8, 2.0)) +
theme_minimal(base_size = 12) +
theme(
axis.text.y = element_blank(),
axis.ticks.y = element_blank(),
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank(),
axis.title.y = element_blank(),
plot.margin = margin(t = 5, r = 10, b = 5, l = 10)
) +
labs(x = "Anzahl erfuellter DSM-5-Kriterien (0-9)", y = NULL)
}
# Datenaufbereitung ####
# Normtabellen liegen als CSV vor: e_<altersgruppe>_<geschlecht>_N<n>.csv, z.B.
# "e_16_30_frauen_N98.csv". Die Stichprobengroesse im Dateinamen ist rein informativ
# und wird nicht hart codiert, sondern per Regex aus dem tatsaechlichen Dateinamen
# gelesen - robuster gegenueber spaeteren Aktualisierungen der Dateien.
csas_e_normtabellen_dateien = if (dir.exists(PFAD_NORMTABELLEN)) {
list.files(PFAD_NORMTABELLEN, pattern = "^e_(16_30|31_49)_(frauen|maenner|gesamt)_N[0-9]+\\.csv$")
} else {
character(0)
}
csas_e_normtabellen = list()
csas_e_normtabellen_fehler = character(0)
for (datei in csas_e_normtabellen_dateien) {
treffer = regmatches(datei, regexec("^e_(16_30|31_49)_(frauen|maenner|gesamt)_N([0-9]+)\\.csv$", datei))[[1]]
altersgruppe = treffer[2]
geschlecht_key = treffer[3]
n = treffer[4]
key = paste0(altersgruppe, "__", geschlecht_key)
pfad = file.path(PFAD_NORMTABELLEN, datei)
eingelesen = tryCatch(
list(ok = TRUE, tab = read.csv(pfad, na.strings = c("", "NA", "-", "—"), stringsAsFactors = FALSE)),
error = function(e) list(ok = FALSE, msg = e$message)
)
if (!isTRUE(eingelesen$ok)) {
csas_e_normtabellen_fehler = c(csas_e_normtabellen_fehler,
paste0(datei, ": Lesefehler (", eingelesen$msg, ")"))
next
}
tab = eingelesen$tab
fehlende_spalten = setdiff(CSAS_NORMTABELLEN_SPALTEN, names(tab))
if (length(fehlende_spalten) > 0) {
csas_e_normtabellen_fehler = c(csas_e_normtabellen_fehler,
paste0(datei, ": fehlende Spalte(n) ", paste(fehlende_spalten, collapse = ", ")))
next
}
tab = tab[order(tab$stanine), ]
csas_e_normtabellen[[key]] = list(
tab = tab,
label = paste0(gsub("_", "-", altersgruppe), " Jahre, ",
c(frauen = "Frauen", maenner = "Maenner", gesamt = "Gesamtstichprobe")[[geschlecht_key]],
" (N=", n, ")")
)
}
# Prozentrang-Textformat in der Tabelle: "0-4", ">4-11", ">96-100". Liefert die
# numerischen Unter-/Obergrenzen fuer die Anzeige eines kombinierten Bereichs.
csas_e_prozentrang_grenzen = function(prozentrang_text) {
von = suppressWarnings(as.numeric(sub("^>?([0-9]+)-.*$", "\\1", prozentrang_text)))
bis = suppressWarnings(as.numeric(sub("^.*-([0-9]+)$", "\\1", prozentrang_text)))
list(von = von, bis = bis)
}
# Sucht die passende Normtabelle nach Altersgruppe (16-30 / 31-49) und Geschlecht.
# Ausserhalb dieser Altersspannen oder bei nicht auswertbarem Alter: kein Normwert
# (keine Naeherung auf die naechstliegende Gruppe - das waere eine stille Annahme).
# Bei nicht eindeutigem Geschlecht: Fallback auf die Gesamtstichprobe, transparent
# als solcher gekennzeichnet.
csas_e_waehle_normtabelle = function(alter, geschlecht_text) {
if (is.null(alter) || length(alter) == 0 || is.na(alter)) {
return(list(ok = FALSE, fehler = "Alter nicht auswertbar - kein Normwert berechenbar."))
}
if (alter >= 16 && alter <= 30) {
altersgruppe = "16_30"
} else if (alter >= 31 && alter <= 49) {
altersgruppe = "31_49"
} else {
return(list(ok = FALSE, fehler = paste0(
"Alter (", alter, ") liegt ausserhalb der digitalisierten Normtabellen (16-49 Jahre)."
)))
}
geschlecht_fallback = NULL
geschlecht_key = if (identical(geschlecht_text, "männlich")) {
"maenner"
} else if (identical(geschlecht_text, "weiblich")) {
"frauen"
} else {
geschlecht_fallback = paste0(
"Geschlecht nicht eindeutig auswertbar - Normwert basiert auf der Gesamtstichprobe."
)
"gesamt"
}
key = paste0(altersgruppe, "__", geschlecht_key)
eintrag = csas_e_normtabellen[[key]]
if (is.null(eintrag)) {
return(list(ok = FALSE, fehler = paste0(
"Normtabelle fuer Altersgruppe ", gsub("_", "-", altersgruppe), " / ", geschlecht_key,
" nicht verfuegbar (Datei fehlt oder fehlerhaft)."
)))
}
list(ok = TRUE, tab = eintrag$tab, label = eintrag$label, geschlecht_fallback = geschlecht_fallback)
}
# Kernlookup: findet die Stanine-Zeile(n), deren [min,max]-Bereich den Rohwert
# enthaelt. Bei mehreren identischen/ueberlappenden Bereichen (typisch bei stark
# bodeneffekt-behafteten Verteilungen, z.B. Rohwert 0 in mehreren Stanine-Baendern)
# wird KEIN einzelner Wert erraten, sondern der volle Stanine-/Prozentrangbereich
# transparent ausgewiesen.
csas_e_stanine_lookup = function(rohwert, tabelle, spalte_praefix) {
if (is.null(rohwert) || length(rohwert) == 0 || is.na(rohwert)) {
return(list(ok = FALSE, fehler = "Rohwert nicht auswertbar."))
}
min_spalte = paste0(spalte_praefix, "_min")
max_spalte = paste0(spalte_praefix, "_max")
passt = rohwert >= tabelle[[min_spalte]] &
(is.na(tabelle[[max_spalte]]) | rohwert <= tabelle[[max_spalte]])
idx = which(passt)
if (length(idx) == 0) {
return(list(ok = FALSE, fehler = paste0(
"Rohwert (", rohwert, ") liegt ausserhalb des Wertebereichs dieser Normtabelle."
)))
}
if (length(idx) == 1) {
return(list(ok = TRUE, eindeutig = TRUE,
stanine = tabelle$stanine[idx], prozentrang = tabelle$prozentrang[idx]))
}
grenze_unten = csas_e_prozentrang_grenzen(tabelle$prozentrang[min(idx)])
grenze_oben = csas_e_prozentrang_grenzen(tabelle$prozentrang[max(idx)])
list(ok = TRUE, eindeutig = FALSE,
stanine_von = min(tabelle$stanine[idx]), stanine_bis = max(tabelle$stanine[idx]),
prozentrang = paste0(grenze_unten$von, "-", grenze_oben$bis))
}
# Oeffentliche Einstiegspunkte (Name/Signatur wie urspruenglich als Erweiterungspunkt
# vorgesehen): CSAS-Summenwert und mittlere taegliche Spielzeit sind zwei getrennt
# genormte Kennwerte in denselben Tabellen, daher zwei Wrapper um denselben Lookup.
berechne_stanine = function(score, alter, geschlecht) {
tab_info = csas_e_waehle_normtabelle(alter, geschlecht)
if (!tab_info$ok) return(list(ok = FALSE, fehler = tab_info$fehler))
ergebnis = csas_e_stanine_lookup(score, tab_info$tab, "summenscore")
c(ergebnis, list(referenzgruppe = tab_info$label, geschlecht_fallback = tab_info$geschlecht_fallback))
}
berechne_stanine_spielzeit = function(minuten, alter, geschlecht) {
tab_info = csas_e_waehle_normtabelle(alter, geschlecht)
if (!tab_info$ok) return(list(ok = FALSE, fehler = tab_info$fehler))
ergebnis = csas_e_stanine_lookup(minuten, tab_info$tab, "spielzeit")
c(ergebnis, list(referenzgruppe = tab_info$label, geschlecht_fallback = tab_info$geschlecht_fallback))
}
# Menschenlesbarer Text fuer einen Stanine-Lookup, gemeinsam von Shiny-UI und
# Word-Export genutzt, damit beide Darstellungen konsistent bleiben.
csas_e_stanine_text = function(stanine_res) {
if (!stanine_res$ok) return(paste0("nicht verfuegbar (", stanine_res$fehler, ")"))
kern = if (isTRUE(stanine_res$eindeutig)) {
paste0("Stanine ", stanine_res$stanine, " / 9 (Prozentrang ", stanine_res$prozentrang, ")")
} else {
paste0("Stanine ", stanine_res$stanine_von, "-", stanine_res$stanine_bis, " / 9 ",
"(bei diesem Rohwert nicht feiner unterscheidbar, Prozentrang ca. ",
stanine_res$prozentrang, ")")
}
ref = paste0(kern, " - Referenzgruppe: ", stanine_res$referenzgruppe)
if (!is.null(stanine_res$geschlecht_fallback)) {
ref = paste0(ref, " (", stanine_res$geschlecht_fallback, ")")
}
ref
}
# 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; white-space: pre-wrap;
}
.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;
}
.alert-hinweis {
background: #ECEFF1; border-left: 5px solid #607D8B;
padding: 10px 16px; border-radius: 4px; color: #37474F;
margin-bottom: 12px; font-size: 0.9em;
}
.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; }
.kontext-zeile {
display: flex; gap: 8px; align-items: baseline;
padding: 4px 0; color: #444; font-size: 0.93em;
}
.kontext-label { font-weight: 600; color: #333; min-width: 220px; }
.geraet-zeile {
display: flex; gap: 10px; align-items: baseline;
padding: 4px 0; color: #444; font-size: 0.93em; border-bottom: 1px solid #F5F5F5;
}
.geraet-name { font-weight: 600; color: #333; min-width: 220px; }
.einordnung-box {
border-radius: 6px; padding: 14px 18px; margin: 12px 0;
border-left: 5px solid;
}
.einordnung-titel { font-weight: 700; font-size: 1.05rem; margin-bottom: 6px; }
.einordnung-hinweis { font-size: 0.93em; line-height: 1.55; }
.einordnung-disclaimer {
font-size: 0.82em; color: #777; font-style: italic;
margin-top: 10px; border-top: 1px solid rgba(0,0,0,.1); padding-top: 8px;
}
.kriterium-zeile {
display: flex; align-items: center; gap: 12px;
padding: 8px 4px; border-bottom: 1px solid #F0F0F0; font-size: 0.92em;
}
.kriterium-zeile.erfuellt { background: #FFF5F5; }
.kriterium-nr { font-weight: 700; color: #8B2635; min-width: 22px; }
.kriterium-name { flex: 1; color: #333; }
.kriterium-items { color: #777; font-size: 0.85em; min-width: 160px; }
.kriterium-status {
font-weight: 700; min-width: 90px; text-align: right;
}
.kriterium-status.ja { color: #B71C1C; }
.kriterium-status.nein { color: #2E7D32; }
.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: #4CAF50; color: white; }
.stufe-badge-1 { background: #F48FB1; color: #333333; }
.stufe-badge-2 { background: #EF5350; color: white; }
.stufe-badge-3 { background: #B71C1C; color: white; }
.score-zahl { font-size: 2.2rem; font-weight: 800; color: #8B2635; }
.score-info { font-size: 0.88em; color: #555; margin-top: 4px; }
"
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("CSAS-E Computerspielabhaengigkeitsskala (Selbstbeurteilung Erwachsene)"),
tags$p("Einzelfall-Auswertung anhand des Testmanuals")
),
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_csas_e_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("CSAS-E - Einzelauswertung", fp_titel)))
doc = body_add_fpar(doc, fpar(
ftext("Chiffre: ", fp_label),
ftext(erg$chiffre, fp_normal),
ftext(" Ausfuelldatum: ", fp_label),
ftext(erg$created_str, 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("Kopfdaten", fp_abschnitt)))
doc = body_add_fpar(doc, fpar(
ftext("Alter: ", fp_label), ftext(erg$alter_text, fp_normal),
ftext(" Geschlecht: ", fp_label), ftext(erg$geschlecht_text, fp_normal)
))
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("Geraetenutzung", fp_abschnitt)))
for (g in erg$geraete) {
doc = body_add_fpar(doc, fpar(
ftext(paste0(g$name, ": "), fp_label), ftext(g$text, fp_normal)
))
}
doc = body_add_par(doc, "", style = "Normal")
if (identical(erg$typ, "kein_spielverhalten")) {
doc = body_add_fpar(doc, fpar(ftext(erg$hinweis_text, fp_normal)))
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext(CSAS_DISCLAIMER, fp_disclaimer)))
return(doc)
}
if (identical(erg$typ, "unvollstaendig")) {
doc = body_add_fpar(doc, fpar(ftext(erg$warnung_text,
fp_text(font.size = 11, bold = TRUE, color = "#BF360C"))))
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext(CSAS_DISCLAIMER, fp_disclaimer)))
return(doc)
}
# typ == "vollstaendig"
einordnung = erg$einordnung
fp_score = fp_text(bold = TRUE, font.size = 12, color = einordnung$farbe)
doc = body_add_fpar(doc, fpar(ftext("Mittlere taegliche Spielzeit", fp_abschnitt)))
doc = body_add_fpar(doc, fpar(ftext(erg$spielzeit_text, fp_normal)))
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("CSAS-Summenwert", fp_abschnitt)))
doc = body_add_fpar(doc, fpar(
ftext(paste0(erg$summenwert, " / 54"), fp_text(bold = TRUE, font.size = 12, color = AKZENT_FARBE))
))
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("DSM-5-Kriterien", fp_abschnitt)))
for (k in erg$kriterien) {
fp_status = if (k$erfuellt)
fp_text(bold = TRUE, font.size = 10, color = "#B71C1C")
else
fp_text(font.size = 10, color = "#2E7D32")
doc = body_add_fpar(doc, fpar(
ftext(paste0(k$nr, ". ", k$name, " (Items ", paste(k$items, collapse = ", "), "): "), fp_normal),
ftext(if (k$erfuellt) "erfuellt" else "nicht erfuellt", fp_status)
))
}
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(
ftext(paste0("Erfuellte Kriterien: ", erg$anzahl_kriterien, " / 9 - ", einordnung$label), fp_score)
))
if (erg$anzahl_kriterien >= 5) {
doc = body_add_fpar(doc, fpar(ftext(CSAS_KONVENTION_HINWEIS,
fp_text(font.size = 9, italic = TRUE, color = "#777777"))))
}
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("Prozentrang-/Stanine-Einordnung", fp_abschnitt)))
doc = body_add_fpar(doc, fpar(
ftext("CSAS-Summenwert: ", fp_label), ftext(csas_e_stanine_text(erg$stanine_summenwert), fp_normal)
))
doc = body_add_fpar(doc, fpar(
ftext("Mittlere taegliche Spielzeit: ", fp_label), ftext(csas_e_stanine_text(erg$stanine_spielzeit), fp_normal)
))
doc = body_add_fpar(doc, fpar(ftext(CSAS_NORMWERT_ALLGEMEIN_HINWEIS,
fp_text(font.size = 9, italic = TRUE, color = "#777777"))))
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("CSAS-E Einzelitems", fp_abschnitt)))
for (i in seq_len(18)) {
stufe = erg$item_stufen[i]
stufe_key = as.character(stufe)
fp_badge = fp_text(
color = CSAS_BADGE_TEXT_FARBEN[[stufe_key]],
bold = TRUE,
shading.color = CSAS_BADGE_FARBEN[[stufe_key]],
font.size = 10
)
doc = body_add_fpar(doc, fpar(
ftext(paste0(i, ". ", erg$item_texte[i], " "), fp_normal),
ftext(paste0(" ", erg$item_antwort_texte[i], " "), fp_badge)
))
}
doc = body_add_par(doc, "", style = "Normal")
if (length(erg$spiele) > 0) {
doc = body_add_fpar(doc, fpar(ftext("Genannte Lieblingsspiele", fp_abschnitt)))
doc = body_add_fpar(doc, fpar(ftext(paste(erg$spiele, collapse = ", "), fp_normal)))
doc = body_add_par(doc, "", style = "Normal")
}
doc = body_add_fpar(doc, fpar(ftext(CSAS_DISCLAIMER, fp_disclaimer)))
doc
}
# Server ####
server = function(input, output, session) {
# --- pseudonym-support-injection v1 ---
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)))
}
})
# Skripte werden NICHT beim App-Start gesourct, nur beim Klick auf "Auswerten".
ergebnis_r = eventReactive(input$btn_suchen, {
chiffre = toupper(trimws(input$chiffre))
if ((nchar(trimws(input$pseudonym)) == 0 && nchar(chiffre) == 0)) {
return(list(typ = "format_fehler",
meldung = "Bitte eine Patientenchiffre eingeben."))
}
if (!(nchar(trimws(input$pseudonym)) > 0 || grepl("^[A-Z][0-9]{6}$", chiffre))) {
return(list(typ = "format_fehler", meldung = paste0(
"Ungueltige Chiffre-Format. Erwartet: ein Grossbuchstabe + 6 Ziffern (z.B. P000123)."
)))
}
if (!file.exists(PFAD_DOWNLOAD_SKRIPT)) {
return(list(typ = "skript_fehler", meldung = paste0(
"Download-Skript nicht gefunden:\n", PFAD_DOWNLOAD_SKRIPT
)))
}
if (!file.exists(PFAD_PSEUDONYM_SKRIPT)) {
return(list(typ = "skript_fehler", meldung = paste0(
"Pseudonym-Skript nicht gefunden:\n", 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 = paste0("Fehler im Download-Skript: ", 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
})
alter_wd = getwd()
on.exit(setwd(alter_wd), add = TRUE)
wd_ziel = if (!is.null(db_ordner)) db_ordner else
dirname(normalizePath(PFAD_PSEUDONYM_SKRIPT, mustWork = FALSE))
setwd(wd_ziel)
ok2 = 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 (!ok2$ok) {
return(list(typ = "skript_fehler", meldung = paste0("Fehler im Pseudonym-Skript: ", ok2$msg)))
}
if (!exists("daten_csas_e", envir = .GlobalEnv)) {
return(list(typ = "skript_fehler", meldung = paste0(
"Objekt 'daten_csas_e' nach dem Sourcen nicht gefunden. Bitte Download-Skript pruefen."
)))
}
if (!exists("pseudo", envir = .GlobalEnv)) {
return(list(typ = "skript_fehler", meldung = paste0(
"Objekt 'pseudo' nach dem Sourcen nicht gefunden. Bitte Pseudonym-Skript pruefen."
)))
}
daten = get("daten_csas_e", envir = .GlobalEnv)
pseudo_df = get("pseudo", envir = .GlobalEnv)
if (!("created" %in% names(daten))) {
return(list(typ = "skript_fehler", meldung = paste0(
"Spalte 'created' (Ausfuelldatum) wurde in 'daten_csas_e' nicht gefunden. ",
"Bitte pruefen, unter welchem Namen der Zeitstempel in dieser formr-Session vorliegt, ",
"und ggf. das Download-Skript oder die Auswertung anpassen."
)))
}
treffer_ps = pseudo_df[pseudo_df$chiffre == chiffre, ]
if (nrow(treffer_ps) == 0) {
return(list(typ = "chiffre_nicht_gefunden", meldung = 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(typ = "chiffre_nicht_gefunden", meldung = paste0(
"Kein CSAS-E-Datensatz fuer Chiffre '", chiffre, "' gefunden. (",
length(alle_session_ids), " Pseudonym(e) geprueft)"
)))
}
info_mehrere = NULL
if (nrow(treffer_dat) > 1) {
n = nrow(treffer_dat)
zeitstempel = sapply(treffer_dat[["created"]], function(x) {
d = csas_e_erstellungsdatum_parsen(x)
if (d$ok) as.numeric(d$dt) else NA_real_
})
treffer_dat = treffer_dat[order(zeitstempel, decreasing = TRUE), ]
info_mehrere = paste0(
"Mehrere Ausfuellungen fuer diese Chiffre gefunden (", n, " Eintraege). ",
"Es wird die Ausfuellung mit dem neuesten Zeitstempel angezeigt."
)
}
zeile = treffer_dat[1, , drop = FALSE]
datum_info = csas_e_erstellungsdatum_parsen(zeile[["created"]])
if (!datum_info$ok) {
return(list(typ = "datum_fehler", meldung = datum_info$fehler))
}
created_str = format(datum_info$dt, "%d.%m.%Y")
created_yyyymmdd = format(datum_info$dt, "%Y%m%d")
# Kopfdaten (nur Anzeige, blockieren die Auswertung nicht bei unbekanntem Format)
alter_roh = zeile[["csas_e_alter"]]
alter_num = suppressWarnings(as.numeric(if (haven::is.labelled(alter_roh)) unclass(alter_roh) else alter_roh))
alter_text = if (length(alter_num) == 0 || is.na(alter_num[1])) "k. A." else as.character(alter_num[1])
geschlecht_res = hole_item_wert(zeile[["csas_e_geschlecht"]], CSAS_GESCHLECHT_CHOICE_TEXTE, "csas_e_geschlecht")
geschlecht_text = if (geschlecht_res$ok && !isTRUE(geschlecht_res$fehlt)) geschlecht_res$text else "k. A."
# Geraetenutzung
geraete = lapply(names(CSAS_GERAETE_SPALTEN), function(key) {
spalte = CSAS_GERAETE_SPALTEN[[key]]
res = pruefe_geraet_nie(zeile[[spalte]], spalte)
list(key = key, name = CSAS_GERAETE_ANZEIGE[[key]], res = res)
})
geraete_fehler = Filter(function(g) !g$res$ok, geraete)
if (length(geraete_fehler) > 0) {
return(list(typ = "item_fehler", meldung = paste0(
"Geraete-Item nicht auswertbar: ", geraete_fehler[[1]]$res$fehler
)))
}
geraete_anzeige = lapply(geraete, function(g) {
txt = if (isTRUE(g$res$fehlt)) "k. A." else g$res$text
list(name = g$name, text = txt)
})
alter_num_wert = if (length(alter_num) == 0 || is.na(alter_num[1])) NA_real_ else alter_num[1]
basis_ergebnis = list(
chiffre = chiffre,
created_str = created_str,
created_yyyymmdd = created_yyyymmdd,
info_mehrere = info_mehrere,
alter_text = alter_text,
alter_num = alter_num_wert,
geschlecht_text = geschlecht_text,
geraete = geraete_anzeige
)
# Fall 1: alle 4 Geraete-Items = "nie" -> kein Spielverhalten, keine weitere Auswertung.
# (Ableitung aus der Bogenlogik/showif, keine woertliche Manualaussage.)
alle_bewertet = all(sapply(geraete, function(g) isTRUE(!g$res$fehlt)))
alle_nie = alle_bewertet && all(sapply(geraete, function(g) isTRUE(g$res$nie)))
if (alle_nie) {
return(c(basis_ergebnis, list(
typ = "kein_spielverhalten",
hinweis_text = paste0(
"Kein Computerspielverhalten in den letzten 12 Monaten berichtet ",
"(alle Gerätetypen 'nie'). CSAS-Summenwert und DSM-5-Kriterien sind fuer ",
"diesen Fall nicht relevant."
)
)))
}
# Fall 2/3: 18 Kernitems pruefen
item_res = lapply(seq_len(18), function(i) {
spalte = paste0("csas_e_", sprintf("%02d", i))
hole_item_wert(zeile[[spalte]], CSAS_ITEM_CHOICE_TEXTE, spalte)
})
item_format_fehler = Filter(function(r) !r$ok, item_res)
if (length(item_format_fehler) > 0) {
return(list(typ = "item_fehler", meldung = paste0(
"Kernitem nicht auswertbar: ", item_format_fehler[[1]]$fehler
)))
}
fehlende_items = which(sapply(item_res, function(r) isTRUE(r$fehlt)))
if (length(fehlende_items) > 0) {
return(c(basis_ergebnis, list(
typ = "unvollstaendig",
warnung_text = paste0(
"Fragebogen unvollstaendig ausgefuellt (", length(fehlende_items), " von 18 Items fehlen: ",
paste(fehlende_items, collapse = ", "), "). Gemaess Testmanual (Kapitel 4.4.4) sollte im ",
"Einzelfall-Setting nur bei vollstaendiger Beantwortung ausgewertet werden."
),
fehlende_items = fehlende_items
)))
}
# Fall 3: vollstaendige Auswertung
item_werte = sapply(item_res, function(r) r$wert)
item_texte = sapply(seq_len(18), function(i) {
spalte = paste0("csas_e_", sprintf("%02d", i))
geparst = csas_e_parse_item_label(daten[[spalte]])
if (!is.na(geparst$text)) geparst$text else paste0("Item ", i)
})
item_antwort_texte = sapply(item_res, function(r) r$text)
summenwert = sum(item_werte)
werktag_p = csas_e_parse_hhmm(zeile[["csas_e_stunden_werktag"]], "csas_e_stunden_werktag")
wochenende_p = csas_e_parse_hhmm(zeile[["csas_e_stunden_wochenende"]], "csas_e_stunden_wochenende")
if (werktag_p$ok && wochenende_p$ok) {
mittlere_minuten = (werktag_p$minuten * 5 + wochenende_p$minuten * 2) / 7
spielzeit_text = paste0(
csas_e_minuten_zu_text(mittlere_minuten), " (", round(mittlere_minuten), " Minuten/Tag im Mittel)"
)
} else {
fehler_texte = c(
if (!werktag_p$ok) werktag_p$fehler else NULL,
if (!wochenende_p$ok) wochenende_p$fehler else NULL
)
mittlere_minuten = NA_real_
spielzeit_text = paste0("Nicht auswertbar: ", paste(fehler_texte, collapse = " "))
}
kriterien = lapply(CSAS_DSM_KRITERIEN, function(k) {
werte = item_werte[k$items]
erfuellt = any(werte == 3, na.rm = TRUE)
c(k, list(erfuellt = erfuellt))
})
anzahl_kriterien = sum(sapply(kriterien, function(k) k$erfuellt))
einordnung = csas_e_einordnung(anzahl_kriterien)
spiele_roh = c(
zeile[["csas_e_spiel1"]][1], zeile[["csas_e_spiel2"]][1], zeile[["csas_e_spiel3"]][1]
)
spiele = vapply(as.character(spiele_roh), bereinige_markdown, character(1), USE.NAMES = FALSE)
spiele = spiele[!is.na(spiele) & nchar(trimws(spiele)) > 0]
stanine_summenwert = berechne_stanine(summenwert, alter_num_wert, geschlecht_text)
stanine_spielzeit = if (!is.na(mittlere_minuten))
berechne_stanine_spielzeit(round(mittlere_minuten), alter_num_wert, geschlecht_text)
else
list(ok = FALSE, fehler = "Mittlere taegliche Spielzeit nicht auswertbar - kein Normwert berechenbar.")
c(basis_ergebnis, list(
typ = "vollstaendig",
summenwert = summenwert,
item_werte = item_werte,
item_texte = item_texte,
item_stufen = item_werte,
item_antwort_texte = item_antwort_texte,
spielzeit_text = spielzeit_text,
mittlere_minuten = mittlere_minuten,
kriterien = kriterien,
anzahl_kriterien = anzahl_kriterien,
einordnung = einordnung,
spiele = spiele,
stanine_summenwert = stanine_summenwert,
stanine_spielzeit = stanine_spielzeit
))
})
output$fehler_ui = renderUI({
req(input$btn_suchen)
d = ergebnis_r()
if (d$typ %in% c("format_fehler", "skript_fehler", "chiffre_nicht_gefunden",
"datum_fehler", "item_fehler")) {
div(class = "alert-fehler", d$meldung)
}
})
output$warnung_ui = renderUI({
req(input$btn_suchen)
d = ergebnis_r()
if (d$typ %in% c("format_fehler", "skript_fehler", "chiffre_nicht_gefunden", "datum_fehler")) {
return(NULL)
}
if (!is.null(d$info_mehrere)) div(class = "alert-warnung", d$info_mehrere)
})
output$ergebnis_ui = renderUI({
req(input$btn_suchen)
d = ergebnis_r()
if (d$typ %in% c("format_fehler", "skript_fehler", "chiffre_nicht_gefunden", "datum_fehler")) {
return(NULL)
}
kopf_ui = tagList(
div(class = "kontext-zeile",
div(class = "kontext-label", "Alter:"), div(d$alter_text)
),
div(class = "kontext-zeile",
div(class = "kontext-label", "Geschlecht:"), div(d$geschlecht_text)
)
)
geraete_ui = lapply(d$geraete, function(g) {
div(class = "geraet-zeile",
div(class = "geraet-name", paste0(g$name, ":")), div(g$text)
)
})
if (d$typ == "kein_spielverhalten") {
return(div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "CSAS-E"),
div(class = "meta-block",
tags$strong("Chiffre: "), d$chiffre,
tags$span(" | ", style = "color:#ccc;"),
tags$strong("Ausfuelldatum: "), d$created_str
),
tags$hr(),
tags$h5("Kopfdaten"), kopf_ui,
tags$hr(),
tags$h5("Geraetenutzung"), geraete_ui,
tags$hr(),
div(class = "alert-hinweis", d$hinweis_text)
))
}
if (d$typ == "unvollstaendig") {
return(div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "CSAS-E"),
div(class = "meta-block",
tags$strong("Chiffre: "), d$chiffre,
tags$span(" | ", style = "color:#ccc;"),
tags$strong("Ausfuelldatum: "), d$created_str
),
tags$hr(),
tags$h5("Kopfdaten"), kopf_ui,
tags$hr(),
tags$h5("Geraetenutzung"), geraete_ui,
tags$hr(),
div(class = "alert-warnung", d$warnung_text)
))
}
# typ == "vollstaendig"
einordnung = d$einordnung
kriterien_ui = lapply(d$kriterien, function(k) {
div(class = paste0("kriterium-zeile", if (k$erfuellt) " erfuellt" else ""),
div(class = "kriterium-nr", paste0(k$nr, ".")),
div(class = "kriterium-name", k$name),
div(class = "kriterium-items", paste0("Items ", paste(k$items, collapse = ", "))),
div(class = paste0("kriterium-status ", if (k$erfuellt) "ja" else "nein"),
if (k$erfuellt) "erfuellt" else "nicht erfuellt")
)
})
items_ui = lapply(seq_len(18), function(i) {
sk = as.character(d$item_werte[i])
div(class = "item-zeile",
div(class = "item-nr", paste0(i, ".")),
div(class = "item-text", d$item_texte[i]),
span(class = paste0("stufe-badge stufe-badge-", sk), d$item_antwort_texte[i])
)
})
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "CSAS-E"),
div(class = "meta-block",
tags$strong("Chiffre: "), d$chiffre,
tags$span(" | ", style = "color:#ccc;"),
tags$strong("Ausfuelldatum: "), d$created_str
),
tags$hr(),
tags$h5("Kopfdaten"), kopf_ui,
tags$hr(),
tags$h5("Geraetenutzung"), geraete_ui,
tags$hr(),
tags$h5("Mittlere taegliche Spielzeit"),
div(d$spielzeit_text),
tags$hr(),
fluidRow(
column(3,
div(
div(class = "score-zahl", d$summenwert),
div("CSAS-Summenwert (0-54)", style = "color:#555;")
)
),
column(9, plotOutput("kriterien_balken", height = "160px"))
),
tags$hr(),
tags$h5("DSM-5-Kriterien"),
div(kriterien_ui),
tags$hr(),
div(class = "einordnung-box",
style = paste0("background:", einordnung$bg, "; border-color:", einordnung$border,
"; color:", einordnung$farbe, ";"),
div(class = "einordnung-titel",
paste0("Erfuellte Kriterien: ", d$anzahl_kriterien, " / 9 ", einordnung$label)),
if (d$anzahl_kriterien >= 5)
div(class = "einordnung-hinweis", CSAS_KONVENTION_HINWEIS),
div(class = "einordnung-disclaimer", CSAS_DISCLAIMER)
),
tags$hr(),
tags$h5("Prozentrang-/Stanine-Einordnung"),
div(class = "kontext-zeile",
div(class = "kontext-label", "CSAS-Summenwert:"),
div(csas_e_stanine_text(d$stanine_summenwert))
),
div(class = "kontext-zeile",
div(class = "kontext-label", "Mittlere taegliche Spielzeit:"),
div(csas_e_stanine_text(d$stanine_spielzeit))
),
div(class = "alert-hinweis", CSAS_NORMWERT_ALLGEMEIN_HINWEIS),
tags$hr(),
tags$h5("CSAS-E Einzelitems"),
div(items_ui),
if (length(d$spiele) > 0) tagList(
tags$hr(),
tags$h5("Genannte Lieblingsspiele"),
div(paste(d$spiele, collapse = ", "))
)
)
})
output$kriterien_balken = renderPlot({
req(input$btn_suchen)
d = ergebnis_r()
req(identical(d$typ, "vollstaendig"))
make_kriterien_balken(d$anzahl_kriterien)
}, bg = "transparent")
output$download_word = downloadHandler(
filename = function() {
d = tryCatch(ergebnis_r(), error = function(e) NULL)
auswertbar = is.list(d) && d$typ %in% c("kein_spielverhalten", "unvollstaendig", "vollstaendig")
if (auswertbar) {
paste0("CSASE_", d$chiffre, "_", d$created_yyyymmdd, ".docx")
} else {
"CSASE_export.docx"
}
},
content = function(file) {
d = tryCatch(ergebnis_r(), error = function(e) NULL)
auswertbar = is.list(d) && d$typ %in% c("kein_spielverhalten", "unvollstaendig", "vollstaendig")
if (!auswertbar) {
doc = read_docx()
meldung = if (is.list(d) && !is.null(d$meldung)) d$meldung else
"Kein Datensatz geladen. Bitte zuerst Chiffre eingeben und 'Auswerten' klicken."
doc = body_add_par(doc, meldung, style = "Normal")
print(doc, target = file)
return()
}
doc = tryCatch(
erstelle_csas_e_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)