# Präambel #### PFAD_DOWNLOAD_SKRIPT = "../API/get_data_bipolar.R" # liefert beim Sourcen: daten_dgbsbipolar PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R" # liefert beim Sourcen: pseudo AKZENT_FARBE = "#8B2635" DGBS_DISCLAIMER = paste0( "Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ", "keine klinische Diagnose. Es handelt sich um eine rein deskriptive Darstellung ", "der Antworten; es liegt keine validierte Auswertungsformel, kein Cutoff-Wert und ", "keine Normtabelle fuer diese spezifische Fragebogenversion vor. Die Interpretation ", "obliegt der behandelnden Person." ) # VORLAEUFIG: Der woertliche Originalwortlaut der Empfehlung aus der Quelle (PDF) # liegt aktuell nicht vor. Dies ist nur eine Paraphrase als Platzhalter und muss # vor dem produktiven Einsatz durch das Original-Zitat ersetzt werden. DGBS_EMPFEHLUNGSTEXT = paste0( "Es wird empfohlen, aerztlichen bzw. psychologischen Rat einzuholen." ) # Angenommene Spaltennamen im formr-Export (Konvention dieser App-Serie). Werden # zur Laufzeit gegen die tatsaechlichen Spalten von daten_dgbsbipolar geprueft # (siehe dgbs_spalte_finden in # Helper ####) - bricht bei Abweichung mit einer # Fehlermeldung ab, die die tatsaechlich vorhandenen Spaltennamen auflistet, # statt stillschweigend falsche Werte zu verwenden. DGBS_SPALTE_SESSION_STANDARD = "session" DGBS_SPALTE_SESSION_ALT = c("session_id", "id", "formr_session") DGBS_SPALTE_DATUM_STANDARD = "created" DGBS_SPALTE_DATUM_ALT = c("ausfuelldatum", "expired", "ended", "modified") library(shiny) library(dplyr) library(haven) library(officer) library(DBI) library(RSQLite) # 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) # EXPLIZITER AUSSCHLUSS: Diese App enthaelt keine API-Keys und keinen direkten # Datenbankzugriff. Sie sourced ausschliesslich die beiden oben genannten externen # Skripte zur Laufzeit (beim Klick auf "Auswerten", nicht beim App-Start). # Helper #### # formr exportiert Frage-/Choice-Texte teils Markdown-escaped (z.B. "01\. " statt # "01. ", weil eine fuehrende Nummerierung sonst als Markdown-Liste interpretiert # wuerde). Entfernt nur den Backslash vor Satzzeichen, sonst nichts. dgbs_unescape_markdown = function(text) { if (is.null(text) || length(text) == 0 || is.na(text[1])) return(text) gsub("\\\\([[:punct:]])", "\\1", as.character(text[1])) } # Entfernt eine fuehrende Item-Nummerierung (z.B. "1. " oder "01) ") aus dem # Fragetext, da diese bereits separat in der item-nr-Spalte angezeigt wird. dgbs_strip_item_nr_praefix = function(text) { if (is.null(text) || length(text) == 0 || is.na(text[1])) return(text) sub("^\\s*[0-9]{1,2}[.)]\\s*", "", as.character(text[1])) } # Liefert den vollen Fragetext aus dem label-Attribut der ORIGINAL-Spalte. dgbs_item_label = function(original_col) { lbl = attr(original_col, "label") if (is.null(lbl) || length(lbl) == 0 || is.na(lbl[1])) return(NA_character_) dgbs_strip_item_nr_praefix(trimws(dgbs_unescape_markdown(lbl[1]))) } # Liefert den Antworttext (aus dem labels-Attribut der ORIGINAL-Spalte) fuer den # tatsaechlich gewaehlten Wert. Arbeitet NIE mit hartkodierten Zahlenwerten, da # die Choice-Reihenfolge in diesem Fragebogen (choice1 = Ja) umgekehrt zur # sonstigen Projektkonvention (choice1 = Nein) ist - die Zuordnung Wert -> Text # kommt ausschliesslich aus den Daten selbst. dgbs_antwort_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(haven::zap_labels(wert[1]))) if (length(pos) > 0) return(trimws(dgbs_unescape_markdown(names(lbl_attr)[pos[1]]))) } NA_character_ } dgbs_ist_ja = function(antwort_text) { isTRUE(!is.na(antwort_text) && identical(trimws(antwort_text), "Ja")) } dgbs_ist_problematisch = function(antwort_text) { isTRUE(!is.na(antwort_text) && identical(trimws(antwort_text), "problematisch")) } # Sucht die tatsaechliche Spalte fuer session-id/Datum in daten_dgbsbipolar: erst # der angenommene Standardname, dann bekannte Alternativen. Bricht mit einer # Fehlermeldung ab, die alle vorhandenen Spaltennamen auflistet, statt eine # falsche Spalte zu raten. dgbs_spalte_finden = function(df, standard, alternativen, beschreibung) { if (standard %in% names(df)) return(standard) for (alt in alternativen) { if (alt %in% names(df)) return(alt) } stop(paste0( "Spalte fuer '", beschreibung, "' nicht gefunden. Erwartet: '", standard, "' (oder Alternativen: ", paste(alternativen, collapse = ", "), "). ", "Tatsaechlich vorhandene Spalten in daten_dgbsbipolar: ", paste(names(df), collapse = ", ") )) } # 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; } .kontext-zeile { display: flex; gap: 8px; align-items: baseline; padding: 4px 0; color: #444; font-size: 0.93em; } .kontext-label { font-weight: 600; color: #333; min-width: 260px; } .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; } .badge-ja, .badge-nein { border-radius: 4px; padding: 2px 11px; font-weight: 700; font-size: 0.82em; white-space: nowrap; display: inline-block; flex-shrink: 0; } .badge-ja { background: #8B2635; color: white; } .badge-nein { background: #E0E0E0; color: #444; } .score-zahl { font-size: 2.0rem; font-weight: 800; color: #8B2635; } " 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("DGBS Bipolar-Fragebogen (Selbsteinschaetzung)"), tags$p("Deutsche Adaptation des Mood Disorder Questionnaire (MDQ), 15 Items") ), 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_dgbsbipolar_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") fp_warnung = fp_text(bold = TRUE, font.size = 11, color = "#C62828") doc = body_add_fpar(doc, fpar(ftext("DGBS Bipolar-Fragebogen (Selbsteinschaetzung)", 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$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("Kennzahl", fp_abschnitt))) doc = body_add_fpar(doc, fpar( ftext(paste0(erg$anzahl_ja, " von 13 Symptomfragen mit Ja beantwortet"), fp_normal) )) doc = body_add_par(doc, "", style = "Normal") doc = body_add_fpar(doc, fpar(ftext("Abschnitt II und III", fp_abschnitt))) doc = body_add_fpar(doc, fpar( ftext(paste0("14. ", erg$item14_text, " "), fp_label), ftext(erg$item14_antwort, fp_normal) )) doc = body_add_fpar(doc, fpar( ftext(paste0("15. ", erg$item15_text, " "), fp_label), ftext(erg$item15_antwort, fp_normal) )) doc = body_add_par(doc, "", style = "Normal") if (isTRUE(erg$item15_problematisch)) { doc = body_add_fpar(doc, fpar(ftext( "VORLAEUFIG (Platzhaltertext, siehe DGBS_EMPFEHLUNGSTEXT im Quellcode):", fp_text(bold = TRUE, font.size = 9, italic = TRUE, color = "#C62828")))) doc = body_add_fpar(doc, fpar(ftext(DGBS_EMPFEHLUNGSTEXT, fp_warnung))) doc = body_add_par(doc, "", style = "Normal") } doc = body_add_fpar(doc, fpar(ftext("Symptomliste (Items 1-13)", fp_abschnitt))) for (i in seq_len(13)) { fp_badge = if (isTRUE(erg$item_ist_ja[i])) fp_text(bold = TRUE, font.size = 10, color = "white", shading.color = AKZENT_FARBE) else fp_text(bold = TRUE, font.size = 10, color = "#444444", shading.color = "#E0E0E0") doc = body_add_fpar(doc, fpar( ftext(paste0(i, ". ", erg$item_texte[i], " "), fp_normal), ftext(paste0(" ", erg$item_antworten[i], " "), fp_badge) )) } doc = body_add_par(doc, "", style = "Normal") doc = body_add_fpar(doc, fpar(ftext(DGBS_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, { 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 = ok$msg)) if (!exists("daten_dgbsbipolar", envir = .GlobalEnv)) { return(list(typ = "objekt_fehlt", meldung = paste0( "Objekt 'daten_dgbsbipolar' wurde nach dem Sourcen von PFAD_DOWNLOAD_SKRIPT nicht gefunden. ", "Bitte Download-Skript pruefen."))) } daten = get("daten_dgbsbipolar", envir = .GlobalEnv) 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)) on.exit(setwd(alter_wd), add = TRUE) setwd(wd_ziel) 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 = ok_ps$msg)) if (!exists("pseudo", envir = .GlobalEnv)) { return(list(typ = "objekt_fehlt", meldung = paste0( "Objekt 'pseudo' wurde nach dem Sourcen von PFAD_PSEUDONYM_SKRIPT nicht gefunden. ", "Bitte Pseudonym-Skript pruefen."))) } pseudo = get("pseudo", envir = .GlobalEnv) # Chiffre-Rueckaufloesung aus Pseudonym, falls Pseudonym eingegeben wurde. if (nchar(trimws(input$pseudonym)) > 0) { pw_treffer = pseudo[pseudo$pseudonym == trimws(input$pseudonym), ] if (nrow(pw_treffer) == 0) { return(list(typ = "pseudonym_nicht_gefunden", meldung = paste0( "Pseudonym '", trimws(input$pseudonym), "' wurde in der Pseudonym-Datenbank nicht gefunden."))) } 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) spalten_ok = tryCatch({ spalte_session = dgbs_spalte_finden(daten, DGBS_SPALTE_SESSION_STANDARD, DGBS_SPALTE_SESSION_ALT, "Session-ID") spalte_datum = dgbs_spalte_finden(daten, DGBS_SPALTE_DATUM_STANDARD, DGBS_SPALTE_DATUM_ALT, "Ausfuelldatum") list(ok = TRUE, session = spalte_session, datum = spalte_datum) }, error = function(e) list(ok = FALSE, msg = e$message)) if (!spalten_ok$ok) return(list(typ = "spalten_fehler", meldung = spalten_ok$msg)) treffer_dat = daten[daten[[spalten_ok$session]] %in% alle_session_ids, ] if (nrow(treffer_dat) == 0) { return(list(typ = "keine_daten", meldung = paste0( "Kein DGBS-Bipolar-Datensatz fuer Chiffre '", chiffre, "' gefunden. (", length(alle_session_ids), " Pseudonym(e) geprueft)"))) } info_mehrere = NULL if (nrow(treffer_dat) > 1) { n = nrow(treffer_dat) treffer_dat = treffer_dat[order(treffer_dat[[spalten_ok$datum]], decreasing = TRUE), ] datum_neu = tryCatch( format(as.POSIXct(treffer_dat[[spalten_ok$datum]][1]), "%d.%m.%Y %H:%M"), error = function(e) "unbekanntes Datum" ) info_mehrere = paste0( "Mehrere Ausfuellungen gefunden (", n, " Eintraege). ", "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[[spalten_ok$datum]][1]), "%d.%m.%Y"), error = function(e) format(Sys.Date(), "%d.%m.%Y") ) # --- Items 1-13 (Abschnitt I, Symptomfragen) --- item_vars_1_13 = paste0("dgbs_bipolar_", sprintf("%02d", 1:13)) item_nr = 1:13 item_texte = sapply(item_vars_1_13, function(v) { t = dgbs_item_label(daten[[v]]) if (is.na(t)) paste0("Item ", v) else t }) item_antworten = sapply(item_vars_1_13, function(v) { a = dgbs_antwort_text(daten[[v]], zeile[[v]]) if (is.na(a)) "k. A." else a }) item_ist_ja = sapply(item_vars_1_13, function(v) { dgbs_ist_ja(dgbs_antwort_text(daten[[v]], zeile[[v]])) }) fehlende_items = item_vars_1_13[item_antworten == "k. A."] anzahl_ja = sum(item_ist_ja, na.rm = TRUE) # --- Item 14 (Abschnitt II) --- item14_text_raw = dgbs_item_label(daten[["dgbs_bipolar_14"]]) item14_text = if (is.na(item14_text_raw)) "Item 14" else item14_text_raw item14_antwort_raw = dgbs_antwort_text(daten[["dgbs_bipolar_14"]], zeile[["dgbs_bipolar_14"]]) item14_antwort = if (is.na(item14_antwort_raw)) "k. A." else item14_antwort_raw # --- Item 15 (Abschnitt III, Problembelastung) --- item15_text_raw = dgbs_item_label(daten[["dgbs_bipolar_15"]]) item15_text = if (is.na(item15_text_raw)) "Item 15" else item15_text_raw item15_antwort_raw = dgbs_antwort_text(daten[["dgbs_bipolar_15"]], zeile[["dgbs_bipolar_15"]]) item15_antwort = if (is.na(item15_antwort_raw)) "k. A." else item15_antwort_raw item15_problematisch = dgbs_ist_problematisch(item15_antwort_raw) list( typ = NULL, chiffre = chiffre, datum_str = datum_str, info_mehrere = info_mehrere, fehlende_items = fehlende_items, item_nr = item_nr, item_texte = item_texte, item_antworten = item_antworten, item_ist_ja = item_ist_ja, anzahl_ja = anzahl_ja, item14_text = item14_text, item14_antwort = item14_antwort, item15_text = item15_text, item15_antwort = item15_antwort, item15_problematisch = item15_problematisch ) }) fehlermeldung_text = function(d) { switch(d$typ, leere_eingabe = d$meldung, format_fehler = paste0( "Ungueltiges Chiffre-Format: '", d$chiffre, "'. Erwartet: ein Grossbuchstabe + 6 Ziffern (z.B. P000123)."), pfad_fehler = d$meldung, skript_fehler = paste0("Fehler beim Ausfuehren eines externen Skripts: ", d$meldung), objekt_fehlt = d$meldung, pseudonym_nicht_gefunden = d$meldung, chiffre_nicht_gefunden = d$meldung, spalten_fehler = d$meldung, keine_daten = d$meldung, "Unbekannter Fehler." ) } output$fehler_ui = renderUI({ req(input$btn_suchen) d = ergebnis_r() if (!is.null(d$typ)) div(class = "alert-fehler", fehlermeldung_text(d)) }) output$warnung_ui = renderUI({ req(input$btn_suchen) d = ergebnis_r() if (!is.null(d$typ)) return(NULL) warnungen = list() if (!is.null(d$info_mehrere)) warnungen = c(warnungen, d$info_mehrere) if (length(d$fehlende_items) > 0) { warnungen = c(warnungen, paste0( "Fehlende/nicht auswertbare Angaben bei: ", paste(d$fehlende_items, collapse = ", "), " - die Kennzahl ist dadurch unvollstaendig.")) } if (length(warnungen) == 0) return(NULL) tagList(lapply(warnungen, function(w) div(class = "alert-warnung", w))) }) output$ergebnis_ui = renderUI({ req(input$btn_suchen) d = ergebnis_r() if (!is.null(d$typ)) return(NULL) items_ui = lapply(seq_along(d$item_nr), function(i) { badge_klasse = if (isTRUE(d$item_ist_ja[i])) "badge-ja" else "badge-nein" div(class = "item-zeile", div(class = "item-nr", paste0(d$item_nr[i], ".")), div(class = "item-text", d$item_texte[i]), span(class = badge_klasse, d$item_antworten[i]) ) }) badge14_klasse = if (identical(d$item14_antwort, "Ja")) "badge-ja" else "badge-nein" badge15_klasse = if (isTRUE(d$item15_problematisch)) "badge-ja" else "badge-nein" tagList( if (isTRUE(d$item15_problematisch)) { div(class = "alert-fehler", tags$strong("Hinweis: Problembelastung angegeben. "), DGBS_EMPFEHLUNGSTEXT ) }, div(class = "abschnitt-karte", div(class = "abschnitt-titel", "Uebersicht"), div(class = "meta-block", tags$strong("Chiffre: "), d$chiffre, tags$span(" | ", style = "color:#ccc;"), tags$strong("Ausfuelldatum: "), d$datum_str ), tags$hr(), div( div(class = "score-zahl", paste0(d$anzahl_ja, " von 13")), div("Symptomfragen mit Ja beantwortet (Items 1-13, rein deskriptiv)", style = "color:#555;") ), tags$hr(), div(class = "kontext-zeile", div(class = "kontext-label", d$item14_text), span(class = badge14_klasse, d$item14_antwort) ), div(class = "kontext-zeile", div(class = "kontext-label", d$item15_text), span(class = badge15_klasse, d$item15_antwort) ) ), div(class = "abschnitt-karte", div(class = "abschnitt-titel", "Symptomliste (Items 1-13)"), div(items_ui) ) ) }) output$download_word = downloadHandler( filename = function() { d = tryCatch(ergebnis_r(), error = function(e) NULL) daten_ok = is.list(d) && is.null(d$typ) chiffre_esc = if (daten_ok && nchar(d$chiffre) > 0) d$chiffre else "export" ausfuelldatum_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("DGBSBipolar_", chiffre_esc, "_", ausfuelldatum_fn, ".docx") }, content = function(file) { d = tryCatch(ergebnis_r(), error = function(e) NULL) daten_ok = is.list(d) && is.null(d$typ) 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_dgbsbipolar_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)