1251 lines
48 KiB
R
1251 lines
48 KiB
R
# 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)
|