# 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 (1–7)", 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 (1–7)", 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)