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

675 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)
library(DBI)
library(RSQLite)
# Hinweis: 'formr' wird nur vom externen Download-Skript benoetigt und dort geladen
# (nicht auf CRAN: remotes::install_github("rubenarslan/formr")). Bewusst kein
# library(formr) hier, damit die App auch ohne dieses Paket startet.
# --- Instrument-Identitaet -------------------------------------------------
# Dies ist der AAQ, 9-Item-ORIGINALVERSION (Hayes et al.), deutsche Fassung
# Ruesch & Brueck 2003 -- NICHT der AAQ-II (10 Items, andere Kodierung, andere
# Range). Keine Verwechslung in Text, Variablennamen oder Kommentaren.
AKZENT_FARBE = "#8B2635"
# Pfade zu den beiden extern gepflegten Skripten.
# pg13r/app.R nutzt "../API/get_data_pg13r.R" bzw. "../get_pseudo.R" -- dieselbe
# Ablage wird hier uebernommen. Falls die Skripte im lokalen Projektverzeichnis
# anders liegen, hier anpassen.
PFAD_DOWNLOAD_SKRIPT = "../API/get_data_aaq9.R" # liefert beim Sourcen: daten_aaq9
PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R" # liefert beim Sourcen: pseudo
# 9 Items, Feldnamen aaq_01 .. aaq_09.
AAQ9_ITEMS = sprintf("aaq_%02d", 1:9)
# 7-stufige Antwortskala, Reihenfolge = Rohwert 1..7.
AAQ9_ANKER = c("nie", "sehr selten", "selten", "manchmal",
"häufig", "fast immer", "immer")
# Umkehr-Items: finaler Wert = 8 - Rohwert. Alle uebrigen: finaler Wert = Rohwert.
AAQ9_UMKEHR_ITEMS = c(2L, 3L, 7L, 8L, 9L)
# Fester Hinweistext (fehlende Normierung).
AAQ9_HINWEIS = paste0(
"Für dieses Instrument liegen keine publizierten Cutoff- oder Normwerte vor. ",
"Der Flexibilitätsscore wird rein deskriptiv berichtet (Range 963, höhere Werte = ",
"mehr psychische Flexibilität/Akzeptanz). Die Interpretation im Einzelfall obliegt ",
"der behandelnden Person, ggf. im Verlauf oder im klinischen Kontext."
)
# Disclaimer-Absatz fuer den Word-Export (bewusst ASCII, keine Umlaute/Sonderzeichen).
AAQ9_DISCLAIMER = paste0(
"Der AAQ (9-Item-Originalversion, deutsche Fassung Ruesch & Brueck 2003) misst erlebnisbezogene ",
"Vermeidung / psychische Flexibilitaet. Fuer dieses Instrument liegen keine publizierten Cutoff- ",
"oder Normwerte vor; der Score wird rein deskriptiv berichtet und ersetzt keine klinische Einschaetzung. ",
"Die Interpretation obliegt der behandelnden Person."
)
# 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)
# Spaltennamen fuer Session-ID und Zeitstempel: aus pg13r/app.R uebernommen
# (dort daten$session bzw. zeile[["created"]]). Gegen den echten daten_aaq9-Header
# nicht verifizierbar -- defensiv geprueft statt blind vorausgesetzt.
AAQ9_SPALTE_SESSION = "session"
AAQ9_SPALTE_CREATED = "created"
# Helper ####
# Entfernt formr-Artefakte aus dem Itemwortlaut: Markdown-Fettung (**...**),
# Markdown-Escapes vor Satzzeichen, fuehrende Item-Nummerierung, Zeilenumbrueche.
aaq9_bereinige_label = function(text) {
if (is.null(text) || length(text) == 0 || is.na(text[1])) return(NA_character_)
t = as.character(text[1])
t = gsub("[\r\n]+", " ", t)
t = gsub("\\*\\*", "", t, fixed = FALSE)
t = gsub("\\\\([[:punct:]])", "\\1", t, perl = TRUE)
t = trimws(t)
t = sub("^\\d+\\s*[.)]?\\s*", "", t)
t = gsub("[[:space:]]+", " ", t)
trimws(t)
}
# Itemwortlaut aus dem label-Attribut der Datenspalte. Fallback: "Item N".
aaq9_item_text = function(daten, var, nr) {
txt = aaq9_bereinige_label(attr(daten[[var]], "label", exact = TRUE))
if (is.na(txt) || nchar(txt) == 0) return(paste0("Item ", nr))
txt
}
# Dekodiert eine einzelne Item-Antwort robust zu Rohwert 1..7 + Ankertext.
# Rueckgabe: list(rohwert = int|NA, anker = chr|NA, ok = TRUE/FALSE).
# Deckt drei Faelle ab (siehe Build-Vorgabe Abschnitt 2):
# 1. numerische / haven_labelled Spalte: Zahl direkt als Rohwert 1..7; ein
# vorhandenes labels-Attribut wird zur Pruefung/Zuordnung ueber den
# Antworttext herangezogen (nicht blind die Zahl nehmen, wenn das Attribut
# abweicht).
# 2. Character-Spalte: exakter Stringvergleich (trimws, ** entfernt, lower)
# gegen die sieben Textanker.
# 3. sonst: ungueltig -> ok = FALSE (kein stillschweigendes NA im Score).
aaq9_dekodiere_item = function(original_spalte, wert) {
leer = list(rohwert = NA_integer_, anker = NA_character_, ok = FALSE)
if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(leer)
# Fall 2: Character
if (is.character(original_spalte) || is.character(wert)) {
txt = tolower(trimws(gsub("\\*\\*", "", as.character(wert[1]))))
idx = match(txt, AAQ9_ANKER)
if (!is.na(idx)) return(list(rohwert = idx, anker = AAQ9_ANKER[idx], ok = TRUE))
return(leer)
}
# Fall 1: numerisch / haven_labelled
num = suppressWarnings(as.numeric(wert[1]))
lbl_attr = attr(original_spalte, "labels")
if (!is.null(lbl_attr) && length(lbl_attr) > 0 && !is.na(num)) {
pos = which(as.vector(lbl_attr) == num)
if (length(pos) > 0) {
lbl_txt = tolower(trimws(gsub("\\*\\*", "", names(lbl_attr)[pos[1]])))
idx = match(lbl_txt, AAQ9_ANKER)
if (!is.na(idx)) return(list(rohwert = idx, anker = AAQ9_ANKER[idx], ok = TRUE))
# labels-Attribut vorhanden, aber Text nicht auf einen Anker abbildbar:
# nicht blind die Zahl uebernehmen -> ungueltig.
return(leer)
}
}
# Kein / kein passendes labels-Attribut: Zahl direkt, nur wenn ganzzahlig 1..7.
if (!is.na(num) && num == round(num) && num >= 1 && num <= 7) {
idx = as.integer(num)
return(list(rohwert = idx, anker = AAQ9_ANKER[idx], ok = TRUE))
}
leer
}
# Klartext zu einem Fehler-/Statuslisten-Objekt aus dem eventReactive.
aaq9_fehlertext = function(erg) {
switch(erg$typ,
leere_eingabe = erg$meldung,
format_fehler = sprintf(
"Ungültige Chiffre '%s'. Erwartet: ein Großbuchstabe + 6 Ziffern (z.B. P000123).",
erg$chiffre
),
skript_fehlt = erg$meldung,
skript_fehler = erg$meldung,
objekt_fehlt = erg$meldung,
spalte_fehlt = erg$meldung,
chiffre_nicht_gefunden = erg$meldung,
keine_daten = erg$meldung,
item_ungueltig = erg$meldung,
"Unbekannter Fehler."
)
}
# Horizontaler Range-Balken 9..63 mit Wertmarkierung. KEINE Farbzonen,
# KEINE Schweregrad-Beschriftung -- rein deskriptive Positionsanzeige.
make_range_plot_aaq9 = function(score) {
ggplot() +
geom_rect(aes(xmin = 9, xmax = 63, ymin = 0, ymax = 1),
fill = "#F0F0F0", color = "#9E9E9E", linewidth = 0.6) +
geom_segment(aes(x = score, xend = score, y = -0.15, yend = 1.15),
color = AKZENT_FARBE, linewidth = 2.6, lineend = "round") +
annotate("text", x = score, y = 1.45,
label = paste0("Flexibilitätsscore: ", score),
color = AKZENT_FARBE, fontface = "bold", size = 4.2) +
scale_x_continuous(limits = c(6, 66), breaks = seq(9, 63, 9)) +
scale_y_continuous(limits = c(-0.6, 1.9)) +
labs(x = "Range 963 (höhere Werte = mehr psychische Flexibilität / Akzeptanz)",
y = NULL) +
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 = 18, b = 12, l = 18)
)
}
# 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.9; 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;
}
.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: 6px; color: #555; font-size: 0.95em; }
.meta-block strong { color: #222; }
.score-zahl { font-size: 2.4rem; font-weight: 800; color: #8B2635; line-height: 1.1; }
.score-label { color: #555; font-size: 0.9em; }
.hinweis-text { font-size: 0.86em; color: #555; line-height: 1.55; }
.item-zeile {
display: flex; align-items: flex-start; gap: 10px;
padding: 8px 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; }
.item-werte {
min-width: 230px; text-align: right; white-space: nowrap;
font-size: 0.85em; font-weight: 600; color: #8B2635; flex-shrink: 0;
}
.umpol-marker { color: #888; font-weight: 500; font-style: italic; }
"
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("AAQ Acceptance and Action Questionnaire (9-Item-Originalversion)"),
tags$p("Hayes et al.; deutsche Fassung Rüsch & Brück 2003 — nicht AAQ-II.")
),
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_aaq9_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_score = fp_text(bold = TRUE, font.size = 12)
fp_warnung = fp_text(font.size = 10, italic = TRUE, color = "#B8860B")
fp_disclaimer = fp_text(font.size = 9, italic = TRUE, color = "#777777")
doc = body_add_fpar(doc, fpar(ftext(
"AAQ (9-Item-Originalversion) - Einzelauswertung", fp_titel
)))
doc = body_add_fpar(doc, fpar(
ftext("Chiffre: ", fp_label), ftext(erg$chiffre, fp_normal),
ftext(" Ausfuelldatum: ", fp_label), ftext(erg$datum_str, fp_normal)
))
if (!is.null(erg$warnung)) {
doc = body_add_fpar(doc, fpar(ftext(erg$warnung, fp_warnung)))
}
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("Flexibilitaetsscore", fp_abschnitt)))
doc = body_add_fpar(doc, fpar(
ftext("Summenwert: ", fp_label),
ftext(paste0(erg$score, " (Range 9-63)"), fp_score)
))
doc = body_add_fpar(doc, fpar(ftext(
"Hoehere Werte = mehr psychische Flexibilitaet / Akzeptanz. Keine publizierten Cutoff- oder Normwerte.",
fp_text(font.size = 9, italic = TRUE, color = "#777777")
)))
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("Einzelitems", fp_abschnitt)))
for (i in seq_along(AAQ9_ITEMS)) {
umgep_txt = if (erg$umgepolt[i]) " (umgepolt)" else ""
roh = if (is.na(erg$rohwerte[i])) "k. A." else as.character(erg$rohwerte[i])
fin = if (is.na(erg$final[i])) "k. A." else as.character(erg$final[i])
anker = if (is.na(erg$anker[i])) "k. A." else erg$anker[i]
doc = body_add_fpar(doc, fpar(
ftext(paste0(i, ". ", erg$item_texte[i], umgep_txt, " "), fp_normal),
ftext(sprintf(" Antwort: %s (%s) | Rohwert: %s | finaler Wert: %s ",
anker, roh, roh, fin),
fp_text(font.size = 10, bold = TRUE, color = AKZENT_FARBE))
))
}
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext(AAQ9_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)))
}
})
# Skripte werden NICHT beim App-Start gesourct, nur beim Klick auf "Auswerten".
ergebnis_r = eventReactive(input$btn_suchen, {
# --- Validierung inline (Zugriff auf input noetig, nicht auslagern) ---
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))
}
# --- Skriptpfade pruefen ---
if (!file.exists(PFAD_DOWNLOAD_SKRIPT)) {
return(list(typ = "skript_fehlt",
meldung = paste0("Download-Skript nicht gefunden:\n", PFAD_DOWNLOAD_SKRIPT)))
}
if (!file.exists(PFAD_PSEUDONYM_SKRIPT)) {
return(list(typ = "skript_fehlt",
meldung = paste0("Pseudonym-Skript nicht gefunden:\n", PFAD_PSEUDONYM_SKRIPT)))
}
# --- Download-Skript sourcen (Return-Value-Pattern) ---
res_dl = tryCatch(
{ source(PFAD_DOWNLOAD_SKRIPT, local = FALSE); list(ok = TRUE) },
error = function(e) list(ok = FALSE, msg = conditionMessage(e))
)
if (!isTRUE(res_dl$ok)) {
return(list(typ = "skript_fehler",
meldung = paste0("Fehler im Download-Skript: ", res_dl$msg)))
}
# --- pseudonyme.db bis zu 5 Ebenen oberhalb des Pseudonym-Skripts suchen ---
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)
res_ps = tryCatch(
{ source(PFAD_PSEUDONYM_SKRIPT, local = FALSE); list(ok = TRUE) },
error = function(e) list(ok = FALSE, msg = conditionMessage(e))
)
if (!isTRUE(res_ps$ok)) {
return(list(typ = "skript_fehler",
meldung = paste0("Fehler im Pseudonym-Skript: ", res_ps$msg)))
}
# --- geforderte Objekte im GlobalEnv? ---
if (!exists("daten_aaq9", envir = .GlobalEnv)) {
return(list(typ = "objekt_fehlt",
meldung = paste0("Objekt 'daten_aaq9' nach dem Sourcen nicht gefunden. ",
"Bitte Download-Skript pruefen.")))
}
if (!exists("pseudo", envir = .GlobalEnv)) {
return(list(typ = "objekt_fehlt",
meldung = paste0("Objekt 'pseudo' nach dem Sourcen nicht gefunden. ",
"Bitte Pseudonym-Skript pruefen.")))
}
daten = get("daten_aaq9", envir = .GlobalEnv)
pseudo = get("pseudo", envir = .GlobalEnv)
if (!(AAQ9_SPALTE_SESSION %in% colnames(daten))) {
return(list(typ = "spalte_fehlt",
meldung = sprintf("Spalte '%s' in daten_aaq9 nicht gefunden. Spaltenname im Download-Skript pruefen.",
AAQ9_SPALTE_SESSION)))
}
if (!(AAQ9_SPALTE_CREATED %in% colnames(daten))) {
return(list(typ = "spalte_fehlt",
meldung = sprintf("Spalte '%s' (Ausfuelldatum) in daten_aaq9 nicht gefunden. Spaltenname im Download-Skript pruefen.",
AAQ9_SPALTE_CREATED)))
}
fehlende_items = setdiff(AAQ9_ITEMS, colnames(daten))
if (length(fehlende_items) > 0) {
return(list(typ = "spalte_fehlt",
meldung = paste0("Item-Spalten fehlen in daten_aaq9: ",
paste(fehlende_items, collapse = ", "))))
}
# --- Chiffre <-> Pseudonym aufloesen, Eindeutigkeits-Override ---
if (nchar(trimws(input$pseudonym)) > 0) {
pw_treffer = pseudo[pseudo$pseudonym == trimws(input$pseudonym), ]
if (nrow(pw_treffer) > 0) chiffre = toupper(trimws(pw_treffer$chiffre[1]))
}
treffer_ps = pseudo[pseudo$chiffre == chiffre, ]
# Kein Pseudonym-Treffer: nur abbrechen, wenn auch kein explizites Pseudonym
# eingegeben wurde. Bei explizitem Pseudonym darf der Lookup direkt ueber die
# Session-ID weiterlaufen (Pseudonym-Bypass).
if (nrow(treffer_ps) == 0 && nchar(trimws(input$pseudonym)) == 0) {
return(list(typ = "chiffre_nicht_gefunden",
meldung = sprintf("Chiffre '%s' wurde in der Pseudonym-Datenbank nicht gefunden.",
chiffre)))
}
alle_session_ids = unique(treffer_ps$pseudonym)
if (nchar(trimws(input$pseudonym)) > 0) alle_session_ids = trimws(input$pseudonym)
chiffre_anzeige = if (nchar(chiffre) > 0) chiffre else trimws(input$pseudonym)
# --- daten_aaq9 nach Session-ID(s) filtern ---
treffer_dat = daten[daten[[AAQ9_SPALTE_SESSION]] %in% alle_session_ids, ]
if (nrow(treffer_dat) == 0) {
return(list(typ = "keine_daten",
meldung = sprintf(
"Keine AAQ-9-Daten zu dieser Session gefunden (Chiffre/Pseudonym: '%s', %d Session-ID(s) geprueft).",
chiffre_anzeige, length(alle_session_ids))))
}
# --- mehrere Ausfuellungen: neueste per created, transparent als Warnung ---
warnung = NULL
if (nrow(treffer_dat) > 1) {
n = nrow(treffer_dat)
treffer_dat = treffer_dat[order(treffer_dat[[AAQ9_SPALTE_CREATED]], decreasing = TRUE), ]
datum_neu = tryCatch(
format(as.POSIXct(treffer_dat[[AAQ9_SPALTE_CREATED]][1]), "%d.%m.%Y %H:%M"),
error = function(e) "unbekanntes Datum"
)
warnung = sprintf(
"Mehrere Ausfüllungen gefunden (%d Einträge). Ausgewertet wird die neueste vom %s.",
n, datum_neu
)
treffer_dat = treffer_dat[1, , drop = FALSE]
}
zeile = treffer_dat[1, , drop = FALSE]
created_roh = zeile[[AAQ9_SPALTE_CREATED]][1]
datum_str = tryCatch(
format(as.POSIXct(created_roh), "%d.%m.%Y"),
error = function(e) "unbekannt"
)
datum_fn = tryCatch(
format(as.Date(as.POSIXct(created_roh)), "%Y%m%d"),
error = function(e) "unbekannt"
)
# --- Items dekodieren ---
rohwerte = integer(9)
anker = character(9)
ok_flags = logical(9)
item_texte = character(9)
for (i in seq_along(AAQ9_ITEMS)) {
var = AAQ9_ITEMS[i]
item_texte[i] = aaq9_item_text(daten, var, i)
dec = aaq9_dekodiere_item(daten[[var]], zeile[[var]])
rohwerte[i] = if (is.na(dec$rohwert)) NA_integer_ else as.integer(dec$rohwert)
anker[i] = dec$anker
ok_flags[i] = isTRUE(dec$ok)
}
if (any(!ok_flags)) {
betroffen = which(!ok_flags)
return(list(typ = "item_ungueltig",
meldung = paste0(
"Mindestens ein Item konnte nicht eindeutig dekodiert werden ",
"(weder als Rohwert 1-7 noch als einer der sieben Textanker): Item ",
paste(betroffen, collapse = ", "), ". ",
"Es wird kein Score berechnet. Bitte Datenexport / Feldformat pruefen.")))
}
# --- Umpolung + Score ---
umgepolt = seq_len(9) %in% AAQ9_UMKEHR_ITEMS
final = ifelse(umgepolt, 8L - rohwerte, rohwerte)
score = sum(final)
list(
typ = "ok",
chiffre = chiffre_anzeige,
datum_str = datum_str,
datum_fn = datum_fn,
warnung = warnung,
item_nr = seq_len(9),
item_texte = item_texte,
rohwerte = rohwerte,
anker = anker,
final = as.integer(final),
umgepolt = umgepolt,
score = score
)
})
output$fehler_ui = renderUI({
req(input$btn_suchen)
erg = ergebnis_r()
if (!identical(erg$typ, "ok")) div(class = "alert-fehler", aaq9_fehlertext(erg))
})
output$warnung_ui = renderUI({
req(input$btn_suchen)
erg = ergebnis_r()
if (!identical(erg$typ, "ok") || is.null(erg$warnung)) return(NULL)
div(class = "alert-warnung", erg$warnung)
})
output$range_plot = renderPlot({
req(input$btn_suchen)
erg = ergebnis_r()
req(identical(erg$typ, "ok"))
make_range_plot_aaq9(erg$score)
}, bg = "transparent")
output$ergebnis_ui = renderUI({
req(input$btn_suchen)
erg = ergebnis_r()
if (!identical(erg$typ, "ok")) return(NULL)
items_ui = lapply(seq_len(9), function(i) {
umpol = if (erg$umgepolt[i]) span(class = "umpol-marker", " (umgepolt)") else NULL
div(class = "item-zeile",
div(class = "item-nr", paste0(i, ".")),
div(class = "item-text", erg$item_texte[i], umpol),
div(class = "item-werte",
sprintf("%s (%d) | Rohwert %d → Wert %d",
erg$anker[i], erg$rohwerte[i], erg$rohwerte[i], erg$final[i]))
)
})
div(
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "AAQ Ergebnisübersicht"),
div(class = "meta-block",
tags$strong("Chiffre: "), erg$chiffre,
tags$span(style = "color:#ccc; margin:0 8px;", "|"),
tags$strong("Ausfülldatum: "), erg$datum_str
)
),
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Flexibilitätsscore"),
fluidRow(
column(3,
div(class = "score-zahl", erg$score),
div(class = "score-label", "Summenwert (Range 963)")
),
column(9, plotOutput("range_plot", height = "150px"))
)
),
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Einzelitems"),
div(
style = "font-size:0.82em; color:#888; margin-bottom:6px;",
"Umkehr-Items 2, 3, 7, 8, 9: finaler Wert = 8 Rohwert. Übrige Items: finaler Wert = Rohwert."
),
div(items_ui)
),
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Hinweis zur Interpretation"),
p(class = "hinweis-text", AAQ9_HINWEIS)
)
)
})
output$download_word = downloadHandler(
filename = function() {
erg = tryCatch(ergebnis_r(), error = function(e) NULL)
if (is.null(erg) || !identical(erg$typ, "ok")) return("AAQ9_Auswertung.docx")
chiffre_esc = gsub("[^A-Za-z0-9]", "", erg$chiffre)
if (nchar(chiffre_esc) == 0) chiffre_esc = "export"
ausfuelldatum_fn = if (is.null(erg$datum_fn) || is.na(erg$datum_fn)) "unbekannt" else erg$datum_fn
paste0("AAQ9_", chiffre_esc, "_", ausfuelldatum_fn, ".docx")
},
content = function(file) {
erg = tryCatch(ergebnis_r(), error = function(e) NULL)
if (is.null(erg) || !identical(erg$typ, "ok")) {
doc = read_docx()
doc = body_add_par(doc,
"Kein auswertbarer Datensatz geladen. Bitte zuerst Chiffre oder Pseudonym eingeben und 'Auswerten' klicken.",
style = "Normal")
print(doc, target = file)
return()
}
doc = tryCatch(
erstelle_aaq9_docx(erg),
error = function(e) {
err_doc = read_docx()
body_add_par(err_doc,
paste0("Fehler beim Erstellen des Word-Dokuments: ", conditionMessage(e)),
style = "Normal")
}
)
print(doc, target = file)
}
)
}
# Start ####
shinyApp(ui = ui, server = server)