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

692 lines
25 KiB
R
Raw Permalink 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 ####
library(shiny)
library(dplyr)
library(ggplot2)
library(haven)
library(officer)
ASF_ANTWORTANKER = c(
"trifft gar nicht zu", "trifft kaum zu", "trifft mittelmäßig zu",
"trifft ziemlich stark zu", "trifft sehr stark zu"
)
ASF_DISCLAIMER = paste0(
"Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ",
"keine klinische Diagnose. Die Perzentilraenge beziehen sich auf die Referenzstichprobe ",
"der Klinik fuer Psychosomatik der RWTH Aachen (N=970). Die Interpretation obliegt der ",
"behandelnden Person."
)
ASF_KONTEXT_ANGST = paste0(
"Zur inhaltlichen Einordnung: In einer Zusatzstichprobe von Angstpatient:innen lag der ",
"ASF-Gesamtmittelwert bei M = 3.01 (Wälte & Kröger, 2000). Dieser Wert dient ausschließlich ",
"als Kontextinformation, nicht als klinischer Vergleichsmaßstab oder Cutoff."
)
# Gleiche Farbpalette wie im pg13r-Stufenbadge (gruen -> dunkelrot), aber umgekehrt
# zugeordnet: hoeherer ASF-Wert = hoehere Selbstwirksamkeit = gruen statt rot.
ASF_BADGE_FARBEN = c(
"1" = "#4A0000",
"2" = "#B71C1C",
"3" = "#EF5350",
"4" = "#F48FB1",
"5" = "#4CAF50"
)
ASF_BADGE_TEXT_FARBEN = c(
"1" = "white",
"2" = "white",
"3" = "white",
"4" = "#333333",
"5" = "white"
)
ASF_BADGE_WORD_FARBEN = list(
"1" = list(bg = "#4A0000", text = "white"),
"2" = list(bg = "#B71C1C", text = "white"),
"3" = list(bg = "#EF5350", text = "white"),
"4" = list(bg = "#F48FB1", text = "#333333"),
"5" = list(bg = "#4CAF50", text = "white")
)
# Infrastruktur ####
PFAD_DOWNLOAD_SKRIPT = "../API/get_data_asf.R" # liefert beim Sourcen: daten_asf
PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R" # liefert beim Sourcen: pseudo
AKZENT_FARBE = "#8B2635"
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)
# Helper ####
# labels-Attribut der ORIGINAL-Spalte (vor Subsetting) lesen, damit
# die Zuordnung Ankertext -> Zahl immer aus den Daten selbst stammt,
# nicht aus einer angenommenen Rohzahl.
asf_resolve_wert = function(original_col, wert) {
if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(NA_real_)
lbl_attr = attr(original_col, "labels")
if (!is.null(lbl_attr) && length(lbl_attr) > 0) {
pos = which(as.vector(lbl_attr) == as.numeric(wert[1]))
if (length(pos) > 0) return(as.numeric(lbl_attr[pos[1]]))
}
as.numeric(wert[1])
}
asf_get_anker_text = function(original_col, wert) {
if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(NA_character_)
lbl_attr = attr(original_col, "labels")
if (!is.null(lbl_attr) && length(lbl_attr) > 0) {
pos = which(as.vector(lbl_attr) == as.numeric(wert[1]))
if (length(pos) > 0) return(names(lbl_attr)[pos[1]])
}
stufe = as.integer(round(as.numeric(wert[1])))
if (is.na(stufe) || stufe < 1L || stufe > 5L) return(NA_character_)
ASF_ANTWORTANKER[stufe]
}
# Entfernt formr-/Markdown-Nummerierungsartefakte am Anfang des Itemtexts
# (z.B. "1. ", "01) ", "**1.** "), die im label-Attribut erscheinen koennen.
clean_item_label = function(text) {
if (is.null(text) || length(text) == 0 || is.na(text[1])) return(NA_character_)
txt = trimws(as.character(text[1]))
txt = sub("^[0-9.)*\\s]+", "", txt)
trimws(txt)
}
# Rundet den aufgeloesten Itemwert auf die naechste Antwortstufe 1-5
# fuer die Badge-Einfaerbung. Ausserhalb des Bereichs -> NA (keine Badge-Farbe).
asf_badge_stufe = function(wert) {
if (is.null(wert) || length(wert) == 0 || is.na(wert)) return(NA_integer_)
stufe = as.integer(round(as.numeric(wert)))
if (stufe < 1L || stufe > 5L) return(NA_integer_)
stufe
}
# Findet die erste Zeile der Tabelle, in der wert zwischen von und bis liegt.
# Randfaelle: unterster Bereich beginnt bei 1.00, oberster endet bei 5.00.
# Kein passendes Intervall (sollte bei Range 1-5 nicht vorkommen) -> NA.
asf_perzentil = function(wert, spalte_von, spalte_bis, tabelle) {
if (is.null(wert) || length(wert) == 0 || is.na(wert)) return(NA_real_)
von = tabelle[[spalte_von]]
bis = tabelle[[spalte_bis]]
treffer = which(wert >= von & wert <= bis)
if (length(treffer) == 0) return(NA_real_)
tabelle$perzentil[treffer[1]]
}
make_gauge_asf = function(wert, perzentil, titel) {
perz_label = if (is.na(perzentil)) "außerhalb der Tabelle" else paste0("Perzentil ", perzentil)
ggplot() +
geom_rect(aes(xmin = 1, xmax = 5, ymin = 0, ymax = 1),
fill = "#F5F5F5", color = "#9E9E9E", linewidth = 0.6) +
geom_segment(aes(x = wert, xend = wert, y = -0.25, yend = 1.25),
color = AKZENT_FARBE, linewidth = 2.5) +
geom_label(aes(x = wert, y = 1.6, label = sprintf("%.2f", wert)),
fill = AKZENT_FARBE, color = "white", fontface = "bold",
linewidth = 0, size = 4) +
annotate("text", x = 3, y = -0.55, label = perz_label,
color = "#555555", size = 3.4) +
scale_x_continuous(limits = c(0.6, 5.4), breaks = 1:5) +
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 = paste0(titel, " (1 = niedrig, 5 = hoch)"), y = NULL)
}
# Datenaufbereitung ####
perzentil_tabelle = data.frame(
perzentil = seq(10, 100, by = 10),
asf_gesamt_von = c(1.00, 2.33, 2.69, 2.90, 3.06, 3.22, 3.40, 3.56, 3.75, 3.96),
asf_gesamt_bis = c(2.32, 2.68, 2.89, 3.05, 3.21, 3.39, 3.55, 3.74, 3.95, 5.00),
asf_1_von = c(1.0, 1.9, 2.6, 2.9, 3.1, 3.4, 3.6, 3.8, 4.0, 4.4),
asf_1_bis = c(1.8, 2.5, 2.8, 3.0, 3.3, 3.5, 3.7, 3.9, 4.3, 5.0),
asf_2_von = c(1.0, 2.3, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.4),
asf_2_bis = c(2.2, 2.7, 2.9, 3.1, 3.3, 3.5, 3.7, 3.9, 4.3, 5.0),
asf_3_von = c(1.0, 1.8, 2.1, 2.4, 2.6, 2.9, 3.1, 3.3, 3.6, 3.9),
asf_3_bis = c(1.7, 2.0, 2.3, 2.5, 2.8, 3.0, 3.2, 3.5, 3.8, 5.0)
)
ASF_SKALEN_ITEMS = list(
skala_1 = c(3, 5, 8, 11, 15),
skala_2 = c(10, 13, 14, 17, 19),
skala_3 = c(2, 4, 9, 16, 18)
)
ASF_SKALEN_LABEL = c(
gesamt = "ASF Gesamtwert",
skala_1 = "Arbeit / Leistung",
skala_2 = "Interaktion",
skala_3 = "Körper / Gesundheit"
)
# UI ####
app_css = "
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f5f5f5; }
.app-header {
background: #8B2635; color: white; padding: 18px 24px 14px;
margin-bottom: 20px; border-radius: 0 0 6px 6px;
}
.app-header h2 { margin: 0; font-size: 1.5rem; font-weight: 600; }
.app-header p { margin: 4px 0 0; opacity: 0.85; font-size: 0.9rem; }
.input-panel {
background: white; border-radius: 6px; padding: 16px 20px;
margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.12);
display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap;
}
.input-panel .form-group { margin-bottom: 0; }
.input-panel label { font-weight: 600; color: #333; }
.btn-laden {
background: #8B2635 !important; color: white !important;
border: none !important; border-radius: 4px !important;
padding: 8px 20px !important; font-weight: 600 !important; cursor: pointer;
}
.btn-laden:hover { background: #6d1e29 !important; }
.alert-fehler {
background: #FFEBEE; border-left: 5px solid #C62828;
padding: 12px 16px; border-radius: 4px; color: #B71C1C;
margin-bottom: 12px; font-weight: 500;
}
.alert-warnung {
background: #FFF3E0; border-left: 5px solid #E65100;
padding: 10px 16px; border-radius: 4px; color: #BF360C;
margin-bottom: 12px; font-size: 0.93em; font-weight: 500;
}
.abschnitt-karte {
background: white; border-radius: 6px; padding: 20px 24px;
margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.12);
}
.abschnitt-titel {
color: #8B2635; font-size: 1.15rem; font-weight: 700;
border-bottom: 2px solid #8B2635; padding-bottom: 8px; margin-bottom: 14px;
}
.meta-block { margin-bottom: 10px; color: #555; font-size: 0.95em; }
.meta-block strong { color: #222; }
.hinweis-zeile {
color: #555; font-size: 0.88em; font-style: italic; margin: 4px 0 14px;
}
.kontext-hinweis {
background: #FAFAFA; border-left: 4px solid #BDBDBD;
padding: 10px 14px; border-radius: 4px; color: #555;
font-size: 0.88em; margin-top: 14px;
}
.wert-block { margin-bottom: 8px; }
.wert-titel { font-weight: 700; color: #333; margin-bottom: 4px; }
.wert-zahl { font-size: 1.9rem; font-weight: 800; color: #8B2635; }
.wert-skala { color: #555; font-size: 0.85em; }
.wert-perzentil {
display: inline-block; margin-top: 4px; font-weight: 600;
color: #555; font-size: 0.9em;
}
.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; }
.antwort-badge {
border-radius: 4px; padding: 2px 9px; font-weight: 700;
font-size: 0.82em; white-space: nowrap; display: inline-block; flex-shrink: 0;
}
.antwort-badge-1 { background: #4A0000; color: white; }
.antwort-badge-2 { background: #B71C1C; color: white; }
.antwort-badge-3 { background: #EF5350; color: white; }
.antwort-badge-4 { background: #F48FB1; color: #333333; }
.antwort-badge-5 { background: #4CAF50; color: white; }
.antwort-badge-na { background: #E0E0E0; color: #555555; }
"
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("ASF Aachener Selbstwirksamkeitsfragebogen"),
tags$p("Wälte & Kröger 2000")
),
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_asf_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("ASF Aachener Selbstwirksamkeitsfragebogen", fp_titel)))
doc = body_add_fpar(doc, fpar(
ftext("Chiffre: ", fp_label),
ftext(erg$chiffre, fp_normal),
ftext(" Ausfülldatum: ", fp_label),
ftext(erg$datum_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("Auswertung", fp_abschnitt)))
doc = body_add_fpar(doc, fpar(
ftext("Höherer Wert = höhere Selbstwirksamkeit. Kein klinischer Cutoff, ",
fp_text(font.size = 9, italic = TRUE, color = "#777777")),
ftext("es handelt sich um Perzentilraenge relativ zur Referenzstichprobe.",
fp_text(font.size = 9, italic = TRUE, color = "#777777"))
))
doc = body_add_par(doc, "", style = "Normal")
for (sk in names(erg$werte)) {
perz = erg$perzentile[[sk]]
perz_txt = if (is.na(perz)) "außerhalb der Tabelle" else paste0("Perzentil ", perz)
doc = body_add_fpar(doc, fpar(
ftext(paste0(ASF_SKALEN_LABEL[[sk]], ": "), fp_label),
ftext(sprintf("%.2f", erg$werte[[sk]]), fp_normal),
ftext(paste0(" (", perz_txt, ")"), fp_normal)
))
}
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext(ASF_KONTEXT_ANGST,
fp_text(font.size = 9, italic = TRUE, color = "#777777"))))
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("ASF Einzelitems", fp_abschnitt)))
for (i in seq_len(20)) {
item_txt = if (!is.na(erg$item_texte[i])) erg$item_texte[i] else paste0("Item ", i)
anker_txt = if (!is.na(erg$anker_texte[i])) erg$anker_texte[i] else "k. A."
stufe = asf_badge_stufe(erg$item_werte[i])
badge_farbe = if (is.na(stufe)) list(bg = "#E0E0E0", text = "#555555") else
ASF_BADGE_WORD_FARBEN[[as.character(stufe)]]
fp_badge = fp_text(
color = badge_farbe$text,
bold = TRUE,
shading.color = badge_farbe$bg,
font.size = 10
)
doc = body_add_fpar(doc, fpar(
ftext(paste0(i, ". ", item_txt, " "), fp_normal),
ftext(paste0(" ", anker_txt, " "), fp_badge)
))
}
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext(ASF_DISCLAIMER, fp_disclaimer)))
doc
}
# Server ####
server = function(input, output, session) {
observe({
query = parseQueryString(session$clientData$url_search)
if (!is.null(query$pseudonym) && nchar(trimws(query$pseudonym)) > 0) {
updateTextInput(session, "pseudonym", value = trimws(query$pseudonym))
}
})
observe({
query = parseQueryString(session$clientData$url_search)
if (!is.null(query$chiffre) && nchar(trimws(query$chiffre)) > 0) {
updateTextInput(session, "chiffre", value = toupper(trimws(query$chiffre)))
}
})
ergebnis_r = eventReactive(input$btn_suchen, {
chiffre = toupper(trimws(input$chiffre))
if (nchar(trimws(input$pseudonym)) == 0 && nchar(chiffre) == 0) {
return(list(typ = "leere_eingabe", meldung = "Bitte Chiffre oder Pseudonym eingeben."))
}
if (nchar(trimws(input$pseudonym)) == 0 && !grepl("^[A-Z][0-9]{6}$", chiffre)) {
return(list(typ = "format_fehler", chiffre = chiffre))
}
if (!file.exists(PFAD_DOWNLOAD_SKRIPT))
return(list(typ = "skript_fehler", meldung = paste0(
"Download-Skript nicht gefunden:\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()
wd_ziel = if (!is.null(db_ordner)) db_ordner else
dirname(normalizePath(PFAD_PSEUDONYM_SKRIPT, mustWork = FALSE))
setwd(wd_ziel)
on.exit(setwd(alter_wd), add = TRUE)
ok_ps = tryCatch({
source(PFAD_PSEUDONYM_SKRIPT, local = FALSE)
list(ok = TRUE)
}, error = function(e) list(ok = FALSE, msg = e$message))
if (!ok_ps$ok) return(list(typ = "skript_fehler", meldung = paste0(
"Fehler im Pseudonym-Skript: ", ok_ps$msg)))
if (!exists("daten_asf", envir = .GlobalEnv))
return(list(typ = "skript_fehler", meldung = paste0(
"Objekt 'daten_asf' nach dem Sourcen nicht gefunden. Bitte Download-Skript prüfen.")))
if (!exists("pseudo", envir = .GlobalEnv))
return(list(typ = "skript_fehler", meldung = paste0(
"Objekt 'pseudo' nach dem Sourcen nicht gefunden. Bitte Pseudonym-Skript prüfen.")))
daten = get("daten_asf", envir = .GlobalEnv)
pseudo_df = get("pseudo", envir = .GlobalEnv)
if (nchar(trimws(input$pseudonym)) > 0) {
pw_wert = trimws(input$pseudonym)
pw_treffer = pseudo_df[pseudo_df$pseudonym == pw_wert, ]
if (nrow(pw_treffer) == 0)
return(list(typ = "kein_treffer", meldung = paste0(
"Pseudonym '", pw_wert, "' wurde in der Pseudonym-Datenbank nicht gefunden.")))
chiffre = toupper(trimws(pw_treffer$chiffre[1]))
}
treffer_ps = pseudo_df[pseudo_df$chiffre == chiffre, ]
if (nrow(treffer_ps) == 0)
return(list(typ = "kein_treffer", 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 = "kein_treffer", meldung = paste0(
"Kein ASF-Datensatz für Chiffre '", chiffre, "' gefunden. (",
length(alle_session_ids), " Pseudonym(e) geprüft)")))
info_mehrere = NULL
if (nrow(treffer_dat) > 1) {
n = nrow(treffer_dat)
treffer_dat = treffer_dat[order(treffer_dat$created, decreasing = TRUE), ]
datum_neu = tryCatch(
format(as.POSIXct(treffer_dat$created[1]), "%d.%m.%Y %H:%M"),
error = function(e) "unbekanntes Datum"
)
info_mehrere = paste0(
"Mehrere Ausfüllungen gefunden (", n, " Einträge). ",
"Angezeigt wird die neueste vom ", datum_neu, "."
)
treffer_dat = treffer_dat[1, , drop = FALSE]
}
zeile = treffer_dat[1, , drop = FALSE]
datum_str = tryCatch(
format(as.POSIXct(zeile[["created"]][1]), "%d.%m.%Y"),
error = function(e) format(Sys.Date(), "%d.%m.%Y")
)
item_vars = paste0("asf_", sprintf("%02d", seq_len(20)))
item_werte = sapply(item_vars, function(var) {
asf_resolve_wert(daten[[var]], zeile[[var]])
})
item_texte = sapply(item_vars, function(var) {
clean_item_label(attr(daten[[var]], "label"))
})
anker_texte = sapply(item_vars, function(var) {
asf_get_anker_text(daten[[var]], zeile[[var]])
})
bereichs_warnungen = character(0)
for (i in seq_len(20)) {
w = item_werte[i]
if (!is.na(w) && (w < 1 || w > 5)) {
bereichs_warnungen = c(bereichs_warnungen, paste0(
"Item asf_", sprintf("%02d", i), " hat einen Wert außerhalb des erwarteten ",
"Bereichs (1-5): ", w
))
}
}
asf_gesamt = round(sum(item_werte) / 20, 2)
asf_1 = round(sum(item_werte[ASF_SKALEN_ITEMS$skala_1]) / 5, 2)
asf_2 = round(sum(item_werte[ASF_SKALEN_ITEMS$skala_2]) / 5, 2)
asf_3 = round(sum(item_werte[ASF_SKALEN_ITEMS$skala_3]) / 5, 2)
werte = list(gesamt = asf_gesamt, skala_1 = asf_1, skala_2 = asf_2, skala_3 = asf_3)
perzentile = list(
gesamt = asf_perzentil(asf_gesamt, "asf_gesamt_von", "asf_gesamt_bis", perzentil_tabelle),
skala_1 = asf_perzentil(asf_1, "asf_1_von", "asf_1_bis", perzentil_tabelle),
skala_2 = asf_perzentil(asf_2, "asf_2_von", "asf_2_bis", perzentil_tabelle),
skala_3 = asf_perzentil(asf_3, "asf_3_von", "asf_3_bis", perzentil_tabelle)
)
warnungen = c(bereichs_warnungen)
if (length(warnungen) == 0) warnungen = NULL
list(
typ = "erfolg",
chiffre = chiffre,
datum_str = datum_str,
info_mehrere = info_mehrere,
werte = werte,
perzentile = perzentile,
item_werte = unname(item_werte),
item_texte = unname(item_texte),
anker_texte = unname(anker_texte),
warnungen = warnungen
)
})
output$fehler_ui = renderUI({
req(input$btn_suchen)
d = ergebnis_r()
meldung = switch(d$typ,
"leere_eingabe" = d$meldung,
"format_fehler" = paste0(
"Ungültige Chiffre '", d$chiffre, "'. Erwartet: ein Großbuchstabe gefolgt von ",
"6 Ziffern (z. B. P000123)."),
"skript_fehler" = d$meldung,
"kein_treffer" = d$meldung,
NULL
)
if (!is.null(meldung)) div(class = "alert-fehler", meldung)
})
output$warnung_ui = renderUI({
req(input$btn_suchen)
d = ergebnis_r()
if (d$typ != "erfolg") return(NULL)
texte = c(d$info_mehrere, d$warnungen)
if (length(texte) == 0) return(NULL)
div(lapply(texte, function(t) div(class = "alert-warnung", t)))
})
output$ergebnis_ui = renderUI({
req(input$btn_suchen)
d = ergebnis_r()
if (d$typ != "erfolg") return(NULL)
items_ui = lapply(seq_len(20), function(i) {
item_txt = if (!is.na(d$item_texte[i])) d$item_texte[i] else paste0("Item ", i)
anker_txt = if (!is.na(d$anker_texte[i])) d$anker_texte[i] else "k. A."
stufe = asf_badge_stufe(d$item_werte[i])
badge_kl = if (is.na(stufe)) "antwort-badge-na" else paste0("antwort-badge-", stufe)
div(class = "item-zeile",
div(class = "item-nr", paste0(i, ".")),
div(class = "item-text", item_txt),
span(class = paste0("antwort-badge ", badge_kl), anker_txt)
)
})
skalen_ui = lapply(names(d$werte), function(sk) {
perz = d$perzentile[[sk]]
perz_txt = if (is.na(perz)) "außerhalb der Tabelle" else paste0("Perzentil ", perz)
fluidRow(
column(3,
div(class = "wert-block",
div(class = "wert-titel", ASF_SKALEN_LABEL[[sk]]),
div(class = "wert-zahl", sprintf("%.2f", d$werte[[sk]])),
div(class = "wert-skala", "Range 1-5"),
div(class = "wert-perzentil", perz_txt)
)
),
column(9, plotOutput(paste0("gauge_", sk), height = "140px"))
)
})
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "ASF Aachener Selbstwirksamkeitsfragebogen"),
div(class = "meta-block",
tags$strong("Chiffre: "), d$chiffre,
tags$span(" | ", style = "color:#ccc;"),
tags$strong("Ausfülldatum: "), d$datum_str
),
div(class = "hinweis-zeile",
"Höherer Wert = höhere Selbstwirksamkeit. Kein klinischer Cutoff ",
"angegeben wird der Perzentilrang relativ zur Referenzstichprobe."
),
tags$hr(),
div(skalen_ui),
div(class = "kontext-hinweis", ASF_KONTEXT_ANGST),
tags$hr(),
tags$h5("ASF Einzelitems"),
div(items_ui)
)
})
output$gauge_gesamt = renderPlot({
req(input$btn_suchen); d = ergebnis_r(); req(d$typ == "erfolg")
make_gauge_asf(d$werte$gesamt, d$perzentile$gesamt, ASF_SKALEN_LABEL[["gesamt"]])
}, bg = "transparent")
output$gauge_skala_1 = renderPlot({
req(input$btn_suchen); d = ergebnis_r(); req(d$typ == "erfolg")
make_gauge_asf(d$werte$skala_1, d$perzentile$skala_1, ASF_SKALEN_LABEL[["skala_1"]])
}, bg = "transparent")
output$gauge_skala_2 = renderPlot({
req(input$btn_suchen); d = ergebnis_r(); req(d$typ == "erfolg")
make_gauge_asf(d$werte$skala_2, d$perzentile$skala_2, ASF_SKALEN_LABEL[["skala_2"]])
}, bg = "transparent")
output$gauge_skala_3 = renderPlot({
req(input$btn_suchen); d = ergebnis_r(); req(d$typ == "erfolg")
make_gauge_asf(d$werte$skala_3, d$perzentile$skala_3, ASF_SKALEN_LABEL[["skala_3"]])
}, bg = "transparent")
output$download_word = downloadHandler(
filename = function() {
d = tryCatch(ergebnis_r(), error = function(e) NULL)
daten_ok = is.list(d) && identical(d$typ, "erfolg")
chiffre_esc = if (daten_ok) gsub("[^A-Za-z0-9]", "_", d$chiffre) else "export"
datum_fn = if (daten_ok)
tryCatch(format(as.Date(d$datum_str, "%d.%m.%Y"), "%Y%m%d"),
error = function(e) format(Sys.Date(), "%Y%m%d"))
else
format(Sys.Date(), "%Y%m%d")
paste0("ASF_", chiffre_esc, "_", datum_fn, ".docx")
},
content = function(file) {
d = tryCatch(ergebnis_r(), error = function(e) NULL)
daten_ok = is.list(d) && identical(d$typ, "erfolg")
if (!daten_ok) {
doc = read_docx()
doc = body_add_par(doc,
"Kein Datensatz geladen. Bitte zuerst Chiffre oder Pseudonym eingeben und 'Auswerten' klicken.",
style = "Normal")
print(doc, target = file)
return()
}
doc = tryCatch(
erstelle_asf_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)