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

648 lines
22 KiB
R
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Präambel ####
library(shiny)
library(dplyr)
library(ggplot2)
library(officer)
PFAD_DOWNLOAD_SKRIPT = "../API/get_data_mlq.R" # liefert: daten_mlq
PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R" # liefert: pseudo
AKZENT_FARBE = "#8B2635"
MLQ_DISCLAIMER = paste0(
"Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ",
"keine klinische Diagnose. Der MLQ ist ein dimensionales Forschungsinstrument ohne ",
"publizierte Cutoffs; die Werte sind deskriptiv zu interpretieren, nicht kategorial. ",
"Die Interpretation obliegt der behandelnden Person."
)
# Textantworten des formr-mc-Feldes -> numerischer Wert 1-7.
MLQ_ANTWORT_TABELLE = c(
"trifft überhaupt nicht zu" = 1,
"weitgehend nicht" = 2,
"eher nicht" = 3,
"teils/teils" = 4,
"eher" = 5,
"weitgehend" = 6,
"ganz genau" = 7
)
MLQ_ITEMS_PRESENCE = c("mlq_01_pr", "mlq_04_pr", "mlq_05_pr", "mlq_06_pr", "mlq_09_pr")
MLQ_ITEMS_SEARCH = c("mlq_02_su", "mlq_03_su", "mlq_07_su", "mlq_08_su", "mlq_10_su")
MLQ_ITEMS_ALLE = c(
"mlq_01_pr", "mlq_02_su", "mlq_03_su", "mlq_04_pr", "mlq_05_pr",
"mlq_06_pr", "mlq_07_su", "mlq_08_su", "mlq_09_pr", "mlq_10_su"
)
# 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)
# Helper ####
# Rekodiert eine einzelne Zelle (Text ODER numerisch) auf 1-7.
# Gibt bei nicht eindeutig zuordenbarem Wert NA + warnung = TRUE zurueck,
# da dieser Pfad (formr liefert Text statt Code) noch nicht produktiv verifiziert ist.
mlq_recodiere_wert = function(rohwert) {
if (is.null(rohwert) || length(rohwert) == 0 || is.na(rohwert[1])) {
return(list(wert = NA_real_, warnung = TRUE, roh_anzeige = "NA"))
}
wert_chr = trimws(as.character(rohwert[1]))
if (nchar(wert_chr) == 0) {
return(list(wert = NA_real_, warnung = TRUE, roh_anzeige = ""))
}
treffer = which(tolower(names(MLQ_ANTWORT_TABELLE)) == tolower(wert_chr))
if (length(treffer) > 0) {
return(list(wert = as.numeric(MLQ_ANTWORT_TABELLE[[treffer[1]]]), warnung = FALSE, roh_anzeige = wert_chr))
}
num_wert = suppressWarnings(as.numeric(wert_chr))
if (!is.na(num_wert) && num_wert %in% 1:7) {
return(list(wert = as.numeric(num_wert), warnung = FALSE, roh_anzeige = wert_chr))
}
list(wert = NA_real_, warnung = TRUE, roh_anzeige = wert_chr)
}
# Itemwortlaut wird zur Laufzeit aus dem label-Attribut der formr-Exportspalte gelesen
# (nicht hartkodiert, da urheberrechtlich geschuetzter Testinhalt).
# formr liefert den Text teils mit eigener Nummerierung ("1. ...") - wird entfernt,
# da die App links davon bereits ihre eigene Itemnummer anzeigt (sonst doppelte Nummerierung).
mlq_item_text = function(daten, spalte) {
if (!(spalte %in% colnames(daten))) return(NA_character_)
et = attr(daten[[spalte]], "label", exact = TRUE)
if (is.null(et) || length(et) == 0 || is.na(et[1])) {
et = attr(daten[[spalte]], "labels", exact = TRUE)
}
if (is.null(et) || length(et) == 0 || is.na(et[1])) return(NA_character_)
text = gsub("\\*\\*", "", trimws(as.character(et[1])), fixed = FALSE)
sub("^\\d+[.)]\\s*", "", trimws(text))
}
# Rekodiert alle 10 Items, invertiert Item 9 und bildet die zwei Subskalenmittelwerte.
berechne_mlq_auswertung = function(zeile, daten) {
item_ergebnisse = list()
warnungen = c()
for (spalte in MLQ_ITEMS_ALLE) {
nr = as.integer(sub("^mlq_(\\d{2})_.*$", "\\1", spalte))
roh = if (spalte %in% colnames(zeile)) zeile[[spalte]][1] else NA
rec = mlq_recodiere_wert(roh)
if (isTRUE(rec$warnung)) {
warnungen = c(warnungen, sprintf(
"Item %d konnte nicht eindeutig zugeordnet werden, Wert war: '%s'", nr, rec$roh_anzeige
))
}
item_ergebnisse[[spalte]] = list(
nr = nr,
spalte = spalte,
wert = rec$wert,
text = mlq_item_text(daten, spalte)
)
}
wert09 = item_ergebnisse[["mlq_09_pr"]]$wert
item09r = if (!is.na(wert09)) 8 - wert09 else NA_real_
presence_werte = c(
item_ergebnisse[["mlq_01_pr"]]$wert,
item_ergebnisse[["mlq_04_pr"]]$wert,
item_ergebnisse[["mlq_05_pr"]]$wert,
item_ergebnisse[["mlq_06_pr"]]$wert,
item09r
)
search_werte = c(
item_ergebnisse[["mlq_02_su"]]$wert,
item_ergebnisse[["mlq_03_su"]]$wert,
item_ergebnisse[["mlq_07_su"]]$wert,
item_ergebnisse[["mlq_08_su"]]$wert,
item_ergebnisse[["mlq_10_su"]]$wert
)
presence_n = sum(!is.na(presence_werte))
search_n = sum(!is.na(search_werte))
list(
item_ergebnisse = item_ergebnisse,
item09r = item09r,
presence = if (presence_n > 0) mean(presence_werte, na.rm = TRUE) else NA_real_,
presence_n = presence_n,
search = if (search_n > 0) mean(search_werte, na.rm = TRUE) else NA_real_,
search_n = search_n,
warnungen = warnungen
)
}
# Reine Positionsanzeige auf 1-7, ohne Zonenfarben/Klassifikation (siehe Projektspezifikation).
make_mlq_position_plot = function(wert, titel) {
p = ggplot() +
geom_rect(aes(xmin = 1, xmax = 7, ymin = 0, ymax = 1), fill = "#EEEEEE", color = "#BBBBBB") +
scale_x_continuous(limits = c(0.5, 7.5), breaks = 1:7) +
scale_y_continuous(limits = c(-0.5, 1.9)) +
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 = titel, y = NULL)
if (!is.na(wert)) {
p = p +
geom_segment(aes(x = wert, xend = wert, y = -0.15, yend = 1.15),
color = AKZENT_FARBE, linewidth = 2.5) +
geom_label(aes(x = wert, y = 1.5, label = sprintf("%.2f", wert)),
fill = AKZENT_FARBE, color = "white", fontface = "bold",
linewidth = 0, size = 4)
}
p
}
# Baut die Klartext-Fehlermeldung aus dem typ-Feld des Ergebnisobjekts.
mlq_fehlermeldung = function(erg) {
if (identical(erg$typ, "format_fehler")) {
return(sprintf(
"Chiffre '%s' hat kein gültiges Format (erwartet: ein Großbuchstabe + 6 Ziffern, z.B. P000123).",
erg$chiffre
))
}
erg$meldung
}
# 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; }
.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; }
.item-wert {
background: #8B2635; color: white; font-weight: 700;
border-radius: 12px; padding: 2px 12px; font-size: 0.9em;
min-width: 20px; text-align: center; flex-shrink: 0; white-space: nowrap;
}
.subskalen-werte { display: flex; gap: 40px; flex-wrap: wrap; margin-bottom: 8px; }
.subskala-zahl { font-size: 2.0rem; font-weight: 800; color: #8B2635; }
.subskala-basis { font-size: 0.85em; color: #555; margin-top: 2px; }
.disclaimer-text { font-size: 0.82em; color: #777; font-style: italic; margin-top: 10px; }
"
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("MLQ Meaning in Life Questionnaire"),
tags$p("Steger, Frazier, Oishi & Kaler (2006) | Presence of Meaning & Search for Meaning")
),
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_mlq_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_warnung = fp_text(font.size = 10, color = "#B8860B")
fp_disclaimer = fp_text(font.size = 9, italic = TRUE, color = "#777777")
doc = body_add_fpar(doc, fpar(
ftext("MLQ - Meaning in Life Questionnaire - Einzelauswertung", fp_titel)
))
doc = body_add_fpar(doc, fpar(
ftext("Chiffre: ", fp_label), ftext(erg$chiffre, fp_normal),
ftext(" Ausfülldatum: ", fp_label), ftext(erg$ausfuelldatum, fp_normal)
))
if (!is.null(erg$mehrfach_hinweis)) {
doc = body_add_fpar(doc, fpar(ftext(erg$mehrfach_hinweis, fp_warnung)))
}
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext("Subskalenwerte", fp_abschnitt)))
doc = body_add_fpar(doc, fpar(
ftext("Presence of Meaning: ", fp_label),
ftext(sprintf("%.2f (aus %d von 5 Items%s)",
erg$presence, erg$presence_n,
if (erg$presence_n < 5) sprintf(", %d Item(s) fehlend", 5 - erg$presence_n) else ""),
fp_normal)
))
doc = body_add_fpar(doc, fpar(
ftext("Search for Meaning: ", fp_label),
ftext(sprintf("%.2f (aus %d von 5 Items%s)",
erg$search, erg$search_n,
if (erg$search_n < 5) sprintf(", %d Item(s) fehlend", 5 - erg$search_n) else ""),
fp_normal)
))
doc = body_add_par(doc, "", style = "Normal")
if (length(erg$warnungen) > 0) {
doc = body_add_fpar(doc, fpar(ftext("Hinweise", fp_abschnitt)))
for (w in erg$warnungen) {
doc = body_add_fpar(doc, fpar(ftext(w, fp_warnung)))
}
doc = body_add_par(doc, "", style = "Normal")
}
doc = body_add_fpar(doc, fpar(ftext("MLQ Einzelitems", fp_abschnitt)))
for (spalte in MLQ_ITEMS_ALLE) {
it = erg$item_ergebnisse[[spalte]]
item_txt = if (!is.na(it$text)) it$text else paste0("Item ", it$nr, " (", spalte, ")")
wert_txt = if (is.na(it$wert)) "fehlend" else sprintf("%.0f", it$wert)
zusatz = if (identical(spalte, "mlq_09_pr")) " [invertiert in Presence-Score]" else ""
doc = body_add_fpar(doc, fpar(
ftext(paste0(it$nr, ". ", item_txt, " "), fp_normal),
ftext(paste0(wert_txt, zusatz), fp_label)
))
}
doc = body_add_par(doc, "", style = "Normal")
doc = body_add_fpar(doc, fpar(ftext(MLQ_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 = "pfad_fehler", meldung = paste0(
"Download-Skript nicht gefunden:\n", PFAD_DOWNLOAD_SKRIPT)))
}
if (!file.exists(PFAD_PSEUDONYM_SKRIPT)) {
return(list(typ = "pfad_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_mlq", envir = .GlobalEnv)) {
return(list(typ = "objekt_fehlt", meldung = paste0(
"Objekt 'daten_mlq' nach dem Sourcen nicht gefunden. Bitte Download-Skript prüfen.")))
}
if (!exists("pseudo", envir = .GlobalEnv)) {
return(list(typ = "objekt_fehlt", meldung = paste0(
"Objekt 'pseudo' nach dem Sourcen nicht gefunden. Bitte Pseudonym-Skript prüfen.")))
}
daten_mlq = get("daten_mlq", envir = .GlobalEnv)
pseudo = get("pseudo", envir = .GlobalEnv)
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, ]
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_mlq[daten_mlq$session %in% alle_session_ids, ]
if (nrow(treffer_dat) == 0) {
return(list(typ = "keine_daten", meldung = paste0(
"Kein MLQ-Datensatz für Chiffre '", chiffre, "' gefunden. ",
"(", length(alle_session_ids), " Pseudonym(e) geprüft)")))
}
mehrfach_hinweis = 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"
)
mehrfach_hinweis = 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]
ausfuelldatum = tryCatch(
format(as.POSIXct(zeile[["created"]][1]), "%d.%m.%Y"),
error = function(e) format(Sys.Date(), "%d.%m.%Y")
)
scores = berechne_mlq_auswertung(zeile, daten_mlq)
list(
typ = "ok",
chiffre = chiffre,
ausfuelldatum = ausfuelldatum,
mehrfach_hinweis = mehrfach_hinweis,
item_ergebnisse = scores$item_ergebnisse,
item09r = scores$item09r,
presence = scores$presence,
presence_n = scores$presence_n,
search = scores$search,
search_n = scores$search_n,
warnungen = scores$warnungen,
meldung = NULL
)
})
output$fehler_ui = renderUI({
req(input$btn_suchen)
erg = ergebnis_r()
if (!identical(erg$typ, "ok")) div(class = "alert-fehler", mlq_fehlermeldung(erg))
})
output$warnung_ui = renderUI({
req(input$btn_suchen)
erg = ergebnis_r()
if (!identical(erg$typ, "ok")) return(NULL)
blocks = list()
if (!is.null(erg$mehrfach_hinweis)) blocks = c(blocks, list(div(class = "alert-warnung", erg$mehrfach_hinweis)))
if (length(erg$warnungen) > 0) {
for (w in erg$warnungen) blocks = c(blocks, list(div(class = "alert-warnung", w)))
}
if (length(blocks) == 0) return(NULL)
div(blocks)
})
output$ergebnis_ui = renderUI({
req(input$btn_suchen)
erg = ergebnis_r()
if (!identical(erg$typ, "ok")) return(NULL)
items_ui = lapply(MLQ_ITEMS_ALLE, function(spalte) {
it = erg$item_ergebnisse[[spalte]]
item_txt = if (!is.na(it$text)) it$text else paste0("Item ", it$nr, " (", spalte, ")")
wert_txt = if (is.na(it$wert)) "" else sprintf("%.0f", it$wert)
zusatz = if (identical(spalte, "mlq_09_pr"))
tags$span(style = "font-style: italic; color: #888; font-size: 0.85em; margin-left: 6px;",
"(invertiert in Presence-Score)")
else NULL
div(class = "item-zeile",
div(class = "item-nr", paste0(it$nr, ".")),
div(class = "item-text", item_txt, zusatz),
div(class = "item-wert", wert_txt)
)
})
presence_basis = sprintf(
"aus %d von 5 Items%s", erg$presence_n,
if (erg$presence_n < 5) paste0(", ", 5 - erg$presence_n, " Item(s) fehlend") else ""
)
search_basis = sprintf(
"aus %d von 5 Items%s", erg$search_n,
if (erg$search_n < 5) paste0(", ", 5 - erg$search_n, " Item(s) fehlend") else ""
)
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "MLQ Meaning in Life Questionnaire"),
div(class = "meta-block",
tags$strong("Chiffre: "), erg$chiffre,
tags$span(" | ", style = "color:#ccc;"),
tags$strong("Ausfülldatum: "), erg$ausfuelldatum
),
tags$hr(),
div(class = "subskalen-werte",
div(
div(class = "subskala-zahl",
if (is.na(erg$presence)) "" else sprintf("%.2f", erg$presence)),
div("Presence of Meaning (17)", style = "color:#555;"),
div(class = "subskala-basis", presence_basis)
),
div(
div(class = "subskala-zahl",
if (is.na(erg$search)) "" else sprintf("%.2f", erg$search)),
div("Search for Meaning (17)", style = "color:#555;"),
div(class = "subskala-basis", search_basis)
)
),
fluidRow(
column(6, plotOutput("presence_plot", height = "140px")),
column(6, plotOutput("search_plot", height = "140px"))
),
tags$hr(),
tags$h5("MLQ Einzelitems"),
div(items_ui),
tags$hr(),
div(class = "disclaimer-text", MLQ_DISCLAIMER)
)
})
output$presence_plot = renderPlot({
req(input$btn_suchen)
erg = ergebnis_r()
req(identical(erg$typ, "ok"))
make_mlq_position_plot(erg$presence, "Presence of Meaning")
}, bg = "transparent")
output$search_plot = renderPlot({
req(input$btn_suchen)
erg = ergebnis_r()
req(identical(erg$typ, "ok"))
make_mlq_position_plot(erg$search, "Search for Meaning")
}, bg = "transparent")
output$download_word = downloadHandler(
filename = function() {
erg = tryCatch(ergebnis_r(), error = function(e) NULL)
if (is.null(erg) || !identical(erg$typ, "ok")) return("MLQ_Auswertung.docx")
chiffre_esc = gsub("[^A-Za-z0-9]", "", erg$chiffre)
datum_fn = tryCatch(
format(as.Date(erg$ausfuelldatum, "%d.%m.%Y"), "%Y%m%d"),
error = function(e) format(Sys.Date(), "%Y%m%d")
)
paste0("MLQ_", chiffre_esc, "_", datum_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 Datensatz geladen. Bitte zuerst Chiffre oder Pseudonym eingeben und 'Auswerten' klicken.",
style = "Normal")
print(doc, target = file)
return()
}
doc = tryCatch(
erstelle_mlq_docx(erg),
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 = ui, server = server)