675 lines
25 KiB
R
675 lines
25 KiB
R
# 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 9–63, 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 9–63 (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 9–63)")
|
||
),
|
||
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)
|